FinancialWaveletAnalyzer.java

package com.morphiqlabs.wavelet.cwt.finance;

import com.morphiqlabs.wavelet.cwt.*;
import com.morphiqlabs.wavelet.api.ContinuousWavelet;
import com.morphiqlabs.wavelet.cwt.finance.MarketAnalysisRequest.AnalysisOptions;

import java.util.*;

/**
 * Comprehensive financial analysis using specialized wavelets.
 * 
 * <p>This analyzer combines Paul, DOG, and Shannon wavelets to provide
 * advanced financial market analysis including:</p>
 * <ul>
 *   <li>Market crash detection and prediction</li>
 *   <li>Volatility clustering analysis</li>
 *   <li>Cyclical pattern identification</li>
 *   <li>Trading signal generation</li>
 *   <li>Risk assessment</li>
 * </ul>
 */
public class FinancialWaveletAnalyzer {
    
    // Object pool for memory optimization
    private final FinancialAnalysisObjectPool pool;
    
    // Analysis result classes
    
    public record CrashDetectionResult(
        List<Integer> crashPoints,
        double[] severity,
        double maxSeverity,
        Map<Integer, Double> crashProbabilities
    ) {
        public List<Integer> getCrashPoints() { return crashPoints; }
        public double getMaxSeverity() { return maxSeverity; }
    }
    
    public enum VolatilityLevel {
        LOW, MEDIUM, HIGH, EXTREME
    }
    
    public record VolatilityCluster(
        int startIndex,
        int endIndex,
        VolatilityLevel level,
        double averageVolatility
    ) {}
    
    public record VolatilityAnalysisResult(
        List<VolatilityCluster> volatilityClusters,
        double[] instantaneousVolatility,
        double averageVolatility,
        double maxVolatility
    ) {
        public List<VolatilityCluster> getVolatilityClusters() { return volatilityClusters; }
    }
    
    public record MarketCycle(
        double period,
        double frequency,
        double strength,
        double phase
    ) {}
    
    public record CyclicalAnalysisResult(
        List<MarketCycle> dominantCycles,
        double[] spectralDensity,
        Map<Double, Double> periodogram
    ) {
        public List<MarketCycle> getDominantCycles() { return dominantCycles; }
    }
    
    public enum MarketRegime {
        TRENDING_UP, TRENDING_DOWN, RANGING, VOLATILE
    }
    
    public enum AnomalyType {
        PRICE_SPIKE, VOLUME_SPIKE, VOLUME_PRICE_DIVERGENCE, UNUSUAL_PATTERN
    }
    
    public record MarketAnomaly(
        int timeIndex,
        AnomalyType type,
        double severity,
        String description
    ) {}
    
    public record MarketAnalysisResult(
        List<Integer> regimeChanges,
        List<MarketAnomaly> anomalies,
        double currentRiskLevel,
        double maxDrawdown,
        Map<Integer, MarketRegime> regimeMap
    ) {
        public List<Integer> getRegimeChanges() { return regimeChanges; }
        public List<MarketAnomaly> getAnomalies() { return anomalies; }
        public double getCurrentRiskLevel() { return currentRiskLevel; }
        public double getMaxDrawdown() { return maxDrawdown; }
    }
    
    public enum SignalType {
        BUY, SELL, HOLD
    }
    
    public record TradingSignal(
        int timeIndex,
        SignalType type,
        double confidence,
        String rationale
    ) {}
    
    public record TradingSignalResult(
        List<TradingSignal> signals,
        double sharpeRatio,
        double winRate
    ) {
        public List<TradingSignal> getSignals() { return signals; }
    }
    
    public record WaveletIndicators(
        double[] trendStrength,
        double[] momentum,
        double[] volatilityIndex,
        double[] supportResistance
    ) {
        public double[] getTrendStrength() { return trendStrength; }
        public double[] getMomentum() { return momentum; }
        public double[] getVolatilityIndex() { return volatilityIndex; }
        public double[] getSupportResistance() { return supportResistance; }
    }
    
    public enum AnalysisObjective {
        CRASH_DETECTION, VOLATILITY_ANALYSIS, CYCLE_DETECTION, SIGNAL_GENERATION
    }
    
    public record OptimalParameters(
        int paulOrder,
        int dogOrder,
        double shannonBandwidth,
        double shannonCenterFreq,
        double[] scaleRange
    ) {
        public int getPaulOrder() { return paulOrder; }
        public int getDogOrder() { return dogOrder; }
        public double getShannonBandwidth() { return shannonBandwidth; }
        public double[] getScaleRange() { return scaleRange; }
    }
    
    // Configuration
    private final CWTConfig config;
    private final FinancialAnalysisParameters parameters;
    
    /**
     * Creates analyzer with default CWT and financial parameters.
     */
    public FinancialWaveletAnalyzer() {
        this(FinancialAnalysisParameters.defaultParameters());
    }
    
    /**
     * Creates analyzer with custom financial parameters and default CWT config.
     */
    public FinancialWaveletAnalyzer(FinancialAnalysisParameters parameters) {
        this(CWTConfig.defaultConfig(), parameters);
    }
    
    /**
     * Creates analyzer with custom CWT and financial parameters.
     */
    public FinancialWaveletAnalyzer(CWTConfig config, FinancialAnalysisParameters parameters) {
        this.config = config;
        this.parameters = parameters;
        this.pool = new FinancialAnalysisObjectPool();
    }
    
    /**
     * Detects market crashes using Paul wavelet for asymmetric pattern recognition.
     */
    public CrashDetectionResult detectMarketCrashes(double[] priceData, double samplingRate) {
        PaulWavelet paul = new PaulWavelet(4); // Order 4 for crash detection
        CWTTransform transform = new CWTTransform(paul, config);
        
        // Use scales that capture sharp drops (1-10 day movements)
        ScaleSpace scales = ScaleSpace.logarithmic(
            parameters.getCrashMinScale(), 
            parameters.getCrashMaxScale(), 
            parameters.getCrashNumScales());
        CWTResult result = transform.analyze(priceData, scales);
        
        // Use pooled array for severity calculations
        FinancialAnalysisObjectPool.ArrayHolder severityHolder = pool.borrowArray(priceData.length);
        
        try {
            double[] severity = severityHolder.array;
            
            // Use pooled int list for crash points
            FinancialAnalysisObjectPool.IntListBuilder crashPointsBuilder = pool.borrowIntList();
            Map<Integer, Double> crashProbabilities = new HashMap<>();
            
            try {
                double[][] coeffs = result.getCoefficients();
                
                // Look for large negative coefficients indicating sharp drops
                for (int t = 1; t < priceData.length - 1; t++) {
                    double asymmetryScore = 0;
                    
                    for (int s = 0; s < scales.getNumScales(); s++) {
                        double coeff = coeffs[s][t];
                        // Paul wavelet responds strongly to asymmetric patterns
                        if (coeff < 0) {
                            asymmetryScore += Math.abs(coeff) * scales.getScale(s);
                        }
                    }
                    
                    severity[t] = asymmetryScore;
                    
                    // Detect crash if asymmetry score exceeds threshold
                    if (asymmetryScore > parameters.getCrashAsymmetryThreshold() && isLocalMaximum(severity, t)) {
                        crashPointsBuilder.add(t);
                        crashProbabilities.put(t, Math.min(asymmetryScore / parameters.getCrashProbabilityNormalization(), 1.0));
                    }
                }
                
                // Calculate max severity
                double maxSeverity = 0;
                for (int i = 0; i < priceData.length; i++) {
                    maxSeverity = Math.max(maxSeverity, severity[i]);
                }
                
                // Return result with copied data
                return new CrashDetectionResult(
                    crashPointsBuilder.toList(), 
                    Arrays.copyOfRange(severity, 0, priceData.length), 
                    maxSeverity, 
                    crashProbabilities
                );
            } finally {
                pool.returnIntList(crashPointsBuilder);
            }
        } finally {
            pool.returnArray(severityHolder);
        }
    }
    
    /**
     * Analyzes volatility clusters using DOG wavelet.
     */
    public VolatilityAnalysisResult analyzeVolatility(double[] priceData, double samplingRate) {
        // Use pooled array for absolute returns
        FinancialAnalysisObjectPool.ArrayHolder absReturnsHolder = pool.borrowArray(priceData.length - 1);
        
        try {
            double[] absReturns = absReturnsHolder.array;
            int absReturnsLength = priceData.length - 1;
            
            // Calculate absolute returns as volatility proxy
            for (int i = 0; i < absReturnsLength; i++) {
                absReturns[i] = Math.abs((priceData[i + 1] - priceData[i]) / priceData[i]);
            }
            
            DOGWavelet dog = new DOGWavelet(2); // Mexican Hat for volatility
            CWTTransform transform = new CWTTransform(dog, config);
            
            // Scales for different volatility horizons
            ScaleSpace scales = ScaleSpace.logarithmic(1.0, 30.0, 15);
            CWTResult result = transform.analyze(Arrays.copyOfRange(absReturns, 0, absReturnsLength), scales);
            
            // Use pooled array for instantaneous volatility
            FinancialAnalysisObjectPool.ArrayHolder volHolder = pool.borrowArray(absReturnsLength);
            
            try {
                double[] instantaneousVolatility = volHolder.array;
                double[][] magnitude = result.getMagnitude();
                
                for (int t = 0; t < absReturnsLength; t++) {
                    double vol = 0;
                    for (int s = 0; s < scales.getNumScales(); s++) {
                        vol += magnitude[s][t] * magnitude[s][t];
                    }
                    instantaneousVolatility[t] = Math.sqrt(vol);
                }
                
                // Identify volatility clusters
                List<VolatilityCluster> clusters = identifyVolatilityClusters(
                    Arrays.copyOfRange(instantaneousVolatility, 0, absReturnsLength));
                
                // Calculate statistics
                double avgVol = 0;
                double maxVol = 0;
                for (int i = 0; i < absReturnsLength; i++) {
                    avgVol += instantaneousVolatility[i];
                    maxVol = Math.max(maxVol, instantaneousVolatility[i]);
                }
                avgVol /= absReturnsLength;
                
                // Return result with a copy of the volatility array
                return new VolatilityAnalysisResult(
                    clusters, 
                    Arrays.copyOfRange(instantaneousVolatility, 0, absReturnsLength), 
                    avgVol, 
                    maxVol
                );
            } finally {
                pool.returnArray(volHolder);
            }
        } finally {
            pool.returnArray(absReturnsHolder);
        }
    }
    
    /**
     * Identifies cyclical patterns using Shannon wavelet.
     */
    public CyclicalAnalysisResult analyzeCyclicalPatterns(double[] priceData, double samplingRate) {
        // Detrend the data first
        double[] detrended = detrendData(priceData);
        
        List<MarketCycle> dominantCycles = new ArrayList<>();
        Map<Double, Double> periodogram = new HashMap<>();
        
        // Test multiple Shannon wavelets with different frequency bands
        double[] testFrequencies = parameters.getCycleTestFrequencies(); // 5, 10, 22, 50 day cycles
        
        for (double testFreq : testFrequencies) {
            double bandwidth = 0.2 * testFreq; // Narrow band around test frequency
            ShannonGaborWavelet shannon = new ShannonGaborWavelet(bandwidth, testFreq / bandwidth);
            
            CWTTransform transform = new CWTTransform(shannon, config);
            ScaleSpace scales = ScaleSpace.linear(0.8, 1.2, 5); // Fine scale resolution
            
            CWTResult result = transform.analyze(detrended, scales);
            double[][] magnitude = result.getMagnitude();
            
            // Calculate average power at this frequency
            double avgPower = 0;
            for (int t = 0; t < detrended.length; t++) {
                for (int s = 0; s < scales.getNumScales(); s++) {
                    avgPower += magnitude[s][t] * magnitude[s][t];
                }
            }
            avgPower /= (detrended.length * scales.getNumScales());
            
            double period = 1.0 / testFreq;
            periodogram.put(period, avgPower);
            
            if (avgPower > 0.01) { // Lower threshold for cycle detection
                // Estimate phase
                double phase = estimatePhase(result);
                dominantCycles.add(new MarketCycle(period, testFreq, avgPower, phase));
            }
        }
        
        // Sort by strength
        dominantCycles.sort((a, b) -> Double.compare(b.strength(), a.strength()));
        
        double[] spectralDensity = new double[priceData.length / 2];
        // Simplified spectral density calculation
        for (int i = 0; i < spectralDensity.length; i++) {
            spectralDensity[i] = 0.1 * Math.random(); // Placeholder
        }
        
        return new CyclicalAnalysisResult(dominantCycles, spectralDensity, periodogram);
    }
    
    
    /**
     * Performs comprehensive market analysis combining all wavelets.
     * 
     * <p>This method uses a parameter object to encapsulate all analysis parameters,
     * providing a cleaner API and better extensibility.</p>
     * 
     * @param request the market analysis request containing all parameters
     * @return comprehensive market analysis results
     * @throws IllegalArgumentException if request parameters are invalid
     */
    public MarketAnalysisResult analyzeMarket(MarketAnalysisRequest request) {
        // Validate request
        if (request == null) {
            throw new IllegalArgumentException("Market analysis request cannot be null");
        }
        
        double[] priceData = request.priceData();
        double[] volumeData = request.volumeData();
        double samplingRate = request.samplingRate();
        AnalysisOptions options = request.options();
        
        // Validate inputs early
        if (priceData == null || priceData.length < 2) {
            throw new IllegalArgumentException("priceData must not be null and must have at least two elements.");
        }
        if (volumeData != null && volumeData.length > 0) {
            if (volumeData.length != priceData.length) {
                throw new IllegalArgumentException("volumeData length (" + volumeData.length + 
                    ") must match priceData length (" + priceData.length + ")");
            }
        }
        if (samplingRate <= 0) {
            throw new IllegalArgumentException("samplingRate must be positive");
        }
        
        // Detect crashes based on options
        CrashDetectionResult crashes = options.detectCrashes() ? 
            detectMarketCrashes(priceData, samplingRate) : 
            new CrashDetectionResult(new ArrayList<>(), new double[0], 0.0, new HashMap<>());
        
        // Analyze volatility based on options
        VolatilityAnalysisResult volatility = options.analyzeVolatility() ? 
            analyzeVolatility(priceData, samplingRate) : 
            createEmptyVolatilityResult(priceData.length);
        
        // Detect regime changes
        List<Integer> regimeChanges = new ArrayList<>();
        Map<Integer, MarketRegime> regimeMap = new HashMap<>();
        MarketRegime currentRegime = MarketRegime.RANGING;
        
        // Note: volatility.instantaneousVolatility has length priceData.length - 1
        // Use explicit bounds check to ensure we never exceed array limits
        int maxIndex = Math.min(priceData.length - 1, volatility.instantaneousVolatility.length);
        for (int i = parameters.getRegimeDetectionLookbackPeriod(); i < maxIndex; i++) {
            MarketRegime newRegime = detectRegime(priceData, i, volatility.instantaneousVolatility[i]);
            if (newRegime != currentRegime) {
                regimeChanges.add(i);
                currentRegime = newRegime;
            }
            regimeMap.put(i, currentRegime);
        }
        
        // Handle the last price point separately (no volatility data for it)
        if (priceData.length >= parameters.getRegimeDetectionLookbackPeriod()) {
            regimeMap.put(priceData.length - 1, currentRegime);
        }
        
        // Detect anomalies
        List<MarketAnomaly> anomalies = new ArrayList<>();
        
        // Volume-price divergence
        for (int i = 1; i < priceData.length - 1; i++) {
            double priceChange = Math.abs(priceData[i] - priceData[i-1]) / priceData[i-1];
            double volumeChange = Math.abs(volumeData[i] - volumeData[i-1]) / volumeData[i-1];
            
            if (volumeChange > parameters.getVolumeDivergenceThreshold() && priceChange < parameters.getPriceDivergenceThreshold()) {
                anomalies.add(new MarketAnomaly(i, AnomalyType.VOLUME_PRICE_DIVERGENCE, 
                    volumeChange, "High volume with minimal price movement"));
            }
        }
        
        // Add crash points as anomalies if crashes were detected
        if (options.detectCrashes()) {
            for (int crashPoint : crashes.crashPoints) {
                anomalies.add(new MarketAnomaly(crashPoint, AnomalyType.PRICE_SPIKE,
                    crashes.severity[crashPoint], "Market crash detected"));
            }
        }
        
        // Calculate risk metrics
        // Volatility array has length priceData.length - 1 (based on returns)
        // Use the last available volatility data point for current risk assessment
        int riskIndex = volatility.instantaneousVolatility.length - 1;
        if (riskIndex < 0) {
            // Handle edge case where volatility data is empty
            throw new IllegalStateException("Cannot calculate risk metrics: volatility data is empty");
        }
        double currentRiskLevel = calculateRiskLevel(volatility, crashes, riskIndex);
        double maxDrawdown = calculateMaxDrawdown(priceData);
        
        return new MarketAnalysisResult(regimeChanges, anomalies, currentRiskLevel, 
                                      maxDrawdown, regimeMap);
    }
    
    /**
     * Generates trading signals based on wavelet analysis.
     */
    public TradingSignalResult generateTradingSignals(double[] priceData, double samplingRate) {
        // Use pooled signal builder
        FinancialAnalysisObjectPool.TradingSignalBuilder signalBuilder = pool.borrowSignalBuilder();
        
        try {
            // Get all analyses
            CrashDetectionResult crashes = detectMarketCrashes(priceData, samplingRate);
            VolatilityAnalysisResult volatility = analyzeVolatility(priceData, samplingRate);
            CyclicalAnalysisResult cycles = analyzeCyclicalPatterns(priceData, samplingRate);
            
            // Generate signals based on combined analysis
            // Ensure we don't exceed volatility array bounds (length = priceData.length - 1)
            // and handle cases where priceData is too short for forward-looking predictions
            // Math.max(0, ...) prevents negative indices when priceData.length < CRASH_PREDICTION_FORWARD_WINDOW
            int maxIndex = Math.max(0, Math.min(priceData.length - parameters.getCrashPredictionForwardWindow(), 
                                               volatility.instantaneousVolatility.length));
            for (int i = parameters.getSignalGenerationMinHistory(); i < maxIndex; i++) {
                // Sell signal before potential crash
                if (crashes.crashProbabilities.containsKey(i + parameters.getCrashPredictionForwardWindow())) {
                    double prob = crashes.crashProbabilities.get(i + parameters.getCrashPredictionForwardWindow());
                    if (prob > 0.7) {
                        signalBuilder.addSignal(i, SignalType.SELL, prob,
                            "High crash probability detected");
                    }
                }
                
                // Buy signal after crash in low volatility
                boolean recentCrash = false;
                int lookbackStart = Math.max(0, i - parameters.getRecentCrashLookbackWindow());
                int lookbackEnd = Math.max(0, i - parameters.getCrashPredictionForwardWindow());
                
                // Only check for recent crashes if we have a valid range
                if (lookbackStart < lookbackEnd) {
                    for (int j = lookbackStart; j < lookbackEnd; j++) {
                        if (crashes.crashPoints.contains(j)) {
                            recentCrash = true;
                            break;
                        }
                    }
                }
                
                if (recentCrash && volatility.instantaneousVolatility[i] < 
                    volatility.averageVolatility * 0.8) {
                    signalBuilder.addSignal(i, SignalType.BUY, 0.75,
                        "Post-crash recovery opportunity");
                }
            }
            
            // Build the signals list
            List<TradingSignal> signals = signalBuilder.build();
            
            // Calculate performance metrics
            double sharpeRatio = calculateSharpeRatio(signals, priceData);
            double winRate = calculateWinRate(signals, priceData);
            
            return new TradingSignalResult(signals, sharpeRatio, winRate);
        } finally {
            pool.returnSignalBuilder(signalBuilder);
        }
    }
    
    /**
     * Calculates wavelet-based technical indicators.
     */
    public WaveletIndicators calculateWaveletIndicators(double[] priceData, double samplingRate) {
        int N = priceData.length;
        double[] trendStrength = new double[N];
        double[] momentum = new double[N];
        double[] volatilityIndex = new double[N];
        double[] supportResistance = new double[N];
        
        // Use Paul wavelet for trend and momentum
        PaulWavelet paul = new PaulWavelet(3);
        CWTTransform paulTransform = new CWTTransform(paul, config);
        ScaleSpace trendScales = ScaleSpace.logarithmic(
            parameters.getTrendMinScale(), 
            parameters.getTrendMaxScale(), 
            parameters.getTrendNumScales());
        CWTResult paulResult = paulTransform.analyze(priceData, trendScales);
        
        // Use DOG wavelet for volatility - analyze absolute returns
        double[] absReturns = new double[N - 1];
        for (int i = 0; i < absReturns.length; i++) {
            absReturns[i] = Math.abs((priceData[i + 1] - priceData[i]) / priceData[i]);
        }
        
        DOGWavelet dog = new DOGWavelet(2);
        CWTTransform dogTransform = new CWTTransform(dog, config);
        ScaleSpace volScales = ScaleSpace.logarithmic(1.0, 20.0, 10);
        CWTResult dogResult = dogTransform.analyze(absReturns, volScales);
        
        double[][] paulCoeffs = paulResult.getCoefficients();
        double[][] dogMagnitude = dogResult.getMagnitude();
        
        for (int t = 0; t < N; t++) {
            // Trend strength from low-frequency Paul coefficients
            double trend = 0;
            for (int s = 5; s < 10 && s < paulCoeffs.length; s++) {
                trend += paulCoeffs[s][t];
            }
            trendStrength[t] = trend / 5.0;
            
            // Momentum from mid-frequency Paul coefficients
            double mom = 0;
            for (int s = 0; s < 5 && s < paulCoeffs.length; s++) {
                mom += paulCoeffs[s][t];
            }
            momentum[t] = mom / 5.0;
            
            // Volatility index from DOG magnitude
            volatilityIndex[t] = calculateVolatilityIndex(t, absReturns, dogMagnitude, volScales);
            
            // Support/Resistance from low-frequency Paul coefficients
            // Low-frequency components represent the underlying trend and key levels
            supportResistance[t] = calculateSupportResistance(paulCoeffs, priceData, t);
        }
        
        return new WaveletIndicators(trendStrength, momentum, volatilityIndex, supportResistance);
    }
    
    /**
     * Optimizes wavelet parameters for specific analysis objective.
     */
    public OptimalParameters optimizeParameters(double[] priceData, AnalysisObjective objective) {
        // Simplified parameter optimization
        OptimizationParameters opt = parameters.getOptimization();
        OptimalParameters params = switch (objective) {
            case CRASH_DETECTION -> new OptimalParameters(
                opt.getCrashPaulOrder(), 
                opt.getCrashDogOrder(), 
                opt.getCrashThresholdFactor(), 
                opt.getCrashSeverityExponent(), 
                opt.getCrashScaleRange()
            );
            case VOLATILITY_ANALYSIS -> new OptimalParameters(
                opt.getVolatilityPaulOrder(), 
                opt.getVolatilityDogOrder(), 
                opt.getVolatilityThresholdFactor(), 
                opt.getVolatilityExponent(), 
                opt.getVolatilityScaleRange()
            );
            case CYCLE_DETECTION -> new OptimalParameters(
                opt.getCycleShannonFb(), 
                opt.getCycleShannonFc(), 
                opt.getCycleThresholdFactor(), 
                opt.getCycleExponent(), 
                opt.getCycleScaleRange()
            );
            case SIGNAL_GENERATION -> new OptimalParameters(
                opt.getSignalPaulOrder(), 
                opt.getSignalDogOrder(), 
                opt.getSignalThresholdFactor(), 
                opt.getSignalExponent(), 
                opt.getSignalScaleRange()
            );
        };
        
        return params;
    }
    
    // Helper methods
    
    private double[] calculateReturns(double[] prices) {
        double[] returns = new double[prices.length - 1];
        for (int i = 0; i < returns.length; i++) {
            returns[i] = (prices[i + 1] - prices[i]) / prices[i];
        }
        return returns;
    }
    
    private boolean isLocalMaximum(double[] data, int index) {
        if (index <= 0 || index >= data.length - 1) return false;
        return data[index] > data[index - 1] && data[index] > data[index + 1];
    }
    
    private List<VolatilityCluster> identifyVolatilityClusters(double[] volatility) {
        List<VolatilityCluster> clusters = new ArrayList<>();
        
        // Calculate average volatility without streams
        double avgVol = 0;
        for (double v : volatility) {
            avgVol += v;
        }
        avgVol /= volatility.length;
        
        int startIdx = -1;
        VolatilityLevel currentLevel = VolatilityLevel.LOW;
        
        for (int i = 0; i < volatility.length; i++) {
            VolatilityLevel level = classifyVolatility(volatility[i], avgVol);
            
            if (level != currentLevel) {
                if (startIdx >= 0 && currentLevel != VolatilityLevel.LOW) {
                    // End current cluster
                    double clusterAvg = 0;
                    for (int j = startIdx; j < i; j++) {
                        clusterAvg += volatility[j];
                    }
                    clusterAvg /= (i - startIdx);
                    
                    clusters.add(new VolatilityCluster(startIdx, i - 1, currentLevel, clusterAvg));
                }
                
                startIdx = i;
                currentLevel = level;
            }
        }
        
        // Handle last cluster
        if (startIdx >= 0 && currentLevel != VolatilityLevel.LOW) {
            double clusterAvg = 0;
            for (int j = startIdx; j < volatility.length; j++) {
                clusterAvg += volatility[j];
            }
            clusterAvg /= (volatility.length - startIdx);
            
            clusters.add(new VolatilityCluster(startIdx, volatility.length - 1, 
                                              currentLevel, clusterAvg));
        }
        
        return clusters;
    }
    
    private VolatilityLevel classifyVolatility(double vol, double avgVol) {
        if (vol < avgVol * parameters.getVolatilityLowThreshold()) return VolatilityLevel.LOW;
        if (vol < avgVol * parameters.getVolatilityMediumThreshold()) return VolatilityLevel.MEDIUM;
        if (vol < avgVol * parameters.getVolatilityHighThreshold()) return VolatilityLevel.HIGH;
        return VolatilityLevel.EXTREME;
    }
    
    private double[] detrendData(double[] data) {
        double[] detrended = new double[data.length];
        
        // Simple linear detrending
        double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
        int n = data.length;
        
        for (int i = 0; i < n; i++) {
            sumX += i;
            sumY += data[i];
            sumXY += i * data[i];
            sumX2 += i * i;
        }
        
        double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        double intercept = (sumY - slope * sumX) / n;
        
        for (int i = 0; i < n; i++) {
            detrended[i] = data[i] - (slope * i + intercept);
        }
        
        return detrended;
    }
    
    private double estimatePhase(CWTResult result) {
        // Simplified phase estimation
        if (result.isComplex()) {
            double[][] phase = result.getPhase();
            return phase[0][phase[0].length / 2]; // Middle point phase
        }
        return 0.0;
    }
    
    private MarketRegime detectRegime(double[] prices, int index, double volatility) {
        // Simple regime detection based on recent price movement and volatility
        if (index < parameters.getRegimeDetectionLookbackPeriod()) return MarketRegime.RANGING;
        
        double recentReturn = (prices[index] - prices[index - parameters.getRegimeDetectionLookbackPeriod()]) / 
                             prices[index - parameters.getRegimeDetectionLookbackPeriod()];
        double avgVolatility = parameters.getDefaultAverageVolatility(); // Assumed average
        
        if (volatility > avgVolatility * 2) {
            return MarketRegime.VOLATILE;
        } else if (recentReturn > parameters.getRegimeTrendThreshold()) {
            return MarketRegime.TRENDING_UP;
        } else if (recentReturn < -parameters.getRegimeTrendThreshold()) {
            return MarketRegime.TRENDING_DOWN;
        } else {
            return MarketRegime.RANGING;
        }
    }
    
    private double calculateRiskLevel(VolatilityAnalysisResult volatility, 
                                    CrashDetectionResult crashes, int currentIndex) {
        double riskLevel = parameters.getBaseRiskLevel(); // Base risk
        
        // Increase risk based on current volatility
        if (currentIndex < volatility.instantaneousVolatility.length) {
            double currentVol = volatility.instantaneousVolatility[currentIndex];
            riskLevel += 0.3 * (currentVol / volatility.maxVolatility);
        }
        
        // Increase risk if recent crash
        for (int crashPoint : crashes.crashPoints) {
            if (Math.abs(crashPoint - currentIndex) < parameters.getRiskAssessmentCrashWindow()) {
                riskLevel += 0.2;
                break;
            }
        }
        
        return Math.min(riskLevel, 1.0);
    }
    
    private double calculateMaxDrawdown(double[] prices) {
        double maxDrawdown = 0;
        double peak = prices[0];
        
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > peak) {
                peak = prices[i];
            } else {
                double drawdown = (peak - prices[i]) / peak;
                maxDrawdown = Math.max(maxDrawdown, drawdown);
            }
        }
        
        return maxDrawdown;
    }
    
    /**
     * Calculates the Sharpe ratio for the generated trading signals.
     * 
     * <p>The Sharpe ratio is calculated as (mean return - risk-free rate) / standard deviation,
     * then annualized using the standard number of trading days per year. The risk-free rate 
     * is taken from the FinancialAnalysisParameters configuration.</p>
     * 
     * @param signals list of trading signals
     * @param prices price data
     * @return Sharpe ratio (annualized)
     */
    private double calculateSharpeRatio(List<TradingSignal> signals, double[] prices) {
        if (signals.isEmpty() || prices.length < 2) {
            return 0.0;
        }
        
        // Use pooled primitive list for returns to avoid boxing
        FinancialAnalysisObjectPool.DoubleListBuilder returnsBuilder = pool.borrowDoubleList();
        
        try {
            double position = 0; // 0 = no position, 1 = long, -1 = short
            int entryIndex = -1;
            double entryPrice = 0;
            
            for (TradingSignal signal : signals) {
                if (signal.timeIndex() >= prices.length) continue;
                
                if (signal.type() == SignalType.BUY && position <= 0) {
                    if (position < 0) {
                        // Close short position
                        double returnPct = (entryPrice - prices[signal.timeIndex()]) / entryPrice;
                        returnsBuilder.add(returnPct);
                    }
                    // Open long position
                    position = 1;
                    entryIndex = signal.timeIndex();
                    entryPrice = prices[entryIndex];
                    
                } else if (signal.type() == SignalType.SELL && position >= 0) {
                    if (position > 0) {
                        // Close long position
                        double returnPct = (prices[signal.timeIndex()] - entryPrice) / entryPrice;
                        returnsBuilder.add(returnPct);
                    }
                    // Open short position
                    position = -1;
                    entryIndex = signal.timeIndex();
                    entryPrice = prices[entryIndex];
                }
            }
            
            // Close any open position at the end
            if (position != 0 && entryIndex >= 0 && entryIndex < prices.length - 1) {
                double finalPrice = prices[prices.length - 1];
                double returnPct = position > 0 
                    ? (finalPrice - entryPrice) / entryPrice
                    : (entryPrice - finalPrice) / entryPrice;
                returnsBuilder.add(returnPct);
            }
            
            if (returnsBuilder.size() == 0) {
                return 0.0;
            }
            
            // Calculate mean and standard deviation using primitive operations
            double meanReturn = 0;
            for (int i = 0; i < returnsBuilder.size(); i++) {
                meanReturn += returnsBuilder.get(i);
            }
            meanReturn /= returnsBuilder.size();
            
            double variance = 0;
            for (int i = 0; i < returnsBuilder.size(); i++) {
                double diff = returnsBuilder.get(i) - meanReturn;
                variance += diff * diff;
            }
            variance /= returnsBuilder.size();
            
            double stdDev = Math.sqrt(variance);
            
            // Convert annual risk-free rate to daily rate
            double dailyRiskFreeRate = parameters.getAnnualRiskFreeRate() / FinancialAnalysisParameters.TRADING_DAYS_PER_YEAR;
            
            // Sharpe ratio = (mean return - risk-free rate) / standard deviation
            // Annualize assuming standard trading days per year
            double excessReturn = meanReturn - dailyRiskFreeRate;
            double dailySharpe = stdDev > 0 ? excessReturn / stdDev : 0.0;
            return dailySharpe * Math.sqrt(FinancialAnalysisParameters.TRADING_DAYS_PER_YEAR);
        } finally {
            pool.returnDoubleList(returnsBuilder);
        }
    }
    
    /**
     * Calculates volatility index at time t using absolute returns and wavelet enhancement.
     * 
     * <p>This method handles the complex indexing between price data (length N) and
     * absolute returns (length N-1), combining raw volatility with wavelet-based
     * enhancement for better detection of volatility patterns.</p>
     * 
     * @param t current time index in price data
     * @param absReturns absolute returns array (length N-1)
     * @param dogMagnitude DOG wavelet magnitude coefficients
     * @param volScales scale space used for volatility analysis
     * @return volatility index value at time t
     */
    private double calculateVolatilityIndex(int t, double[] absReturns, double[][] dogMagnitude, 
                                          ScaleSpace volScales) {
        // Note: absReturns is N-1 length, so we need to handle indexing carefully
        if (t == 0) {
            // First point: use the first return volatility
            return absReturns.length > 0 ? absReturns[0] * 100.0 : 0.0;
        } else if (t < absReturns.length) {
            // For t > 0 and t < absReturns.length, use raw absolute return plus wavelet enhancement
            double baseVol = absReturns[t-1] * 100.0; // Scale up for visibility
            
            // Add wavelet-based enhancement for better detection
            if (t-1 < dogMagnitude[0].length) {
                double waveletEnhancement = 0;
                int volScalesUsed = Math.min(3, volScales.getNumScales());
                for (int s = 0; s < volScalesUsed; s++) {
                    waveletEnhancement += dogMagnitude[s][t-1] * dogMagnitude[s][t-1];
                }
                return baseVol + Math.sqrt(waveletEnhancement);
            } else {
                return baseVol;
            }
        } else {
            // Last element: can't calculate new volatility, so use previous value
            // This handles the case where t == priceData.length - 1
            return t > 0 ? calculateVolatilityIndex(t-1, absReturns, dogMagnitude, volScales) : 0.0;
        }
    }
    
    /**
     * Calculates support/resistance levels using wavelet analysis.
     * 
     * <p>This method identifies key price levels by analyzing low-frequency wavelet
     * coefficients which represent the underlying market structure. Local extrema
     * in these coefficients often correspond to support and resistance levels.</p>
     * 
     * @param paulCoeffs wavelet coefficients from Paul transform
     * @param prices original price data
     * @param currentIndex current time index
     * @return estimated support/resistance level at current index
     */
    private double calculateSupportResistance(double[][] paulCoeffs, double[] prices, int currentIndex) {
        if (currentIndex == 0 || paulCoeffs.length == 0) {
            return prices[currentIndex];
        }
        
        // Use low-frequency scales (higher indices) for structure
        int structureScaleStart = Math.min(5, paulCoeffs.length - 1);
        int structureScaleEnd = Math.min(10, paulCoeffs.length);
        
        // Look for local extrema in a window around current point
        int windowSize = parameters.getSupportResistanceWindow();
        int startIdx = Math.max(0, currentIndex - windowSize);
        int endIdx = Math.min(paulCoeffs[0].length - 1, currentIndex + windowSize);
        
        double closestSupport = prices[currentIndex];
        double closestResistance = prices[currentIndex];
        double minDistance = Double.MAX_VALUE;
        
        // Find local maxima (resistance) and minima (support) in wavelet space
        for (int scale = structureScaleStart; scale < structureScaleEnd; scale++) {
            if (scale >= paulCoeffs.length) break;
            
            double[] coeffs = paulCoeffs[scale];
            
            for (int i = startIdx + 1; i < endIdx - 1 && i < coeffs.length - 1; i++) {
                // Check for local maximum (potential resistance)
                if (coeffs[i] > coeffs[i-1] && coeffs[i] > coeffs[i+1]) {
                    double level = prices[i];
                    double distance = Math.abs(level - prices[currentIndex]);
                    
                    if (level > prices[currentIndex] && distance < minDistance) {
                        closestResistance = level;
                        minDistance = distance;
                    }
                }
                
                // Check for local minimum (potential support)
                if (coeffs[i] < coeffs[i-1] && coeffs[i] < coeffs[i+1]) {
                    double level = prices[i];
                    double distance = Math.abs(level - prices[currentIndex]);
                    
                    if (level < prices[currentIndex] && distance < minDistance) {
                        closestSupport = level;
                        minDistance = distance;
                    }
                }
            }
        }
        
        // Return the closest support or resistance level
        double supportDist = Math.abs(prices[currentIndex] - closestSupport);
        double resistDist = Math.abs(prices[currentIndex] - closestResistance);
        
        // If we're closer to support, return support; otherwise resistance
        if (supportDist < resistDist) {
            return closestSupport;
        } else {
            return closestResistance;
        }
    }
    
    /**
     * Calculates the win rate of trading signals.
     * 
     * @param signals list of trading signals
     * @param prices price data
     * @return win rate (0.0 to 1.0)
     */
    private double calculateWinRate(List<TradingSignal> signals, double[] prices) {
        if (signals.isEmpty() || prices.length < 2) {
            return 0.0;
        }
        
        int winningTrades = 0;
        int totalTrades = 0;
        double position = 0;
        double entryPrice = 0;
        
        for (TradingSignal signal : signals) {
            if (signal.timeIndex() >= prices.length) continue;
            
            if (signal.type() == SignalType.BUY && position <= 0) {
                if (position < 0) {
                    // Close short position
                    double profit = entryPrice - prices[signal.timeIndex()];
                    if (profit > 0) winningTrades++;
                    totalTrades++;
                }
                // Open long position
                position = 1;
                entryPrice = prices[signal.timeIndex()];
                
            } else if (signal.type() == SignalType.SELL && position >= 0) {
                if (position > 0) {
                    // Close long position
                    double profit = prices[signal.timeIndex()] - entryPrice;
                    if (profit > 0) winningTrades++;
                    totalTrades++;
                }
                // Open short position
                position = -1;
                entryPrice = prices[signal.timeIndex()];
            }
        }
        
        // Check final position
        if (position != 0 && prices.length > 0) {
            double finalPrice = prices[prices.length - 1];
            double profit = position > 0 
                ? finalPrice - entryPrice
                : entryPrice - finalPrice;
            if (profit > 0) winningTrades++;
            totalTrades++;
        }
        
        return totalTrades > 0 ? (double) winningTrades / totalTrades : 0.0;
    }
    
    /**
     * Creates an empty volatility result for when volatility analysis is disabled.
     * 
     * @param signalLength length of the price signal
     * @return empty volatility analysis result
     */
    private VolatilityAnalysisResult createEmptyVolatilityResult(int signalLength) {
        int volLength = Math.max(0, signalLength - 1);
        double[] emptyArray = new double[volLength];
        List<VolatilityCluster> emptyClusters = new ArrayList<>();
        return new VolatilityAnalysisResult(
            emptyClusters,   // volatilityClusters
            emptyArray,      // instantaneousVolatility
            0.0,             // averageVolatility
            0.0              // maxVolatility
        );
    }
    
    /**
     * Gets the current financial analysis parameters.
     * @return the parameters configuration
     */
    public FinancialAnalysisParameters getParameters() {
        return parameters;
    }
    
    /**
     * Gets the CWT configuration.
     * @return the CWT config
     */
    public CWTConfig getConfig() {
        return config;
    }
    
    /**
     * Gets the object pool statistics for monitoring.
     * @return the pool statistics
     */
    public FinancialAnalysisObjectPool.PoolStatistics getPoolStatistics() {
        return pool.getStatistics();
    }
}