FinancialAnalyzer.java

package com.morphiqlabs.financial;

import com.morphiqlabs.wavelet.modwt.MODWTTransform;
import com.morphiqlabs.wavelet.modwt.MODWTResult;
import com.morphiqlabs.wavelet.api.Daubechies;
import com.morphiqlabs.wavelet.api.BoundaryMode;

/**
 * Financial analyzer that uses wavelet transforms to analyze financial time series.
 * 
 * <p>This class provides configurable financial analysis capabilities including
 * market crash detection, volatility analysis, and regime change detection using
 * wavelet-based methods.</p>
 * 
 * <p>All analysis methods use configurable thresholds provided through the
 * {@link FinancialAnalysisConfig} rather than hardcoded values, allowing
 * customization for different markets and instruments.</p>
 */
public final class FinancialAnalyzer {
    
    private static final double EPSILON = 1e-10; // Tolerance for zero comparison
    
    private final FinancialAnalysisConfig config;
    private final MODWTTransform transform;
    
    /**
     * Creates a new FinancialAnalyzer with the specified configuration.
     * 
     * @param config the analysis configuration (not null)
     * @throws IllegalArgumentException if config is null
     */
    public FinancialAnalyzer(FinancialAnalysisConfig config) {
        if (config == null) {
            throw new IllegalArgumentException("Configuration cannot be null");
        }
        this.config = config;
        this.transform = new MODWTTransform(Daubechies.DB4, BoundaryMode.PERIODIC);
    }
    
    
    /**
     * Analyzes crash asymmetry in the financial time series.
     * 
     * <p>Uses wavelet analysis to detect asymmetric behavior that may indicate
     * market crashes. The analysis compares positive and negative movements
     * in the detail coefficients.</p>
     * 
     * @param prices the price time series (must not be null)
     * @return the asymmetry score; values above the configured threshold indicate potential crashes
     * @throws IllegalArgumentException if prices is null or invalid length
     */
    public double analyzeCrashAsymmetry(double[] prices) {
        validatePrices(prices);
        
        // Convert prices to returns
        double[] returns = calculateReturns(prices);
        
        // Perform wavelet transform
        MODWTResult result = transform.forward(returns);
        double[] details = result.detailCoeffs();
        
        // Calculate asymmetry between positive and negative movements
        double positiveSum = 0.0;
        double negativeSum = 0.0;
        int positiveCount = 0;
        int negativeCount = 0;
        
        for (double detail : details) {
            if (detail > 0) {
                positiveSum += detail;
                positiveCount++;
            } else if (detail < 0) {
                negativeSum += Math.abs(detail);
                negativeCount++;
            }
        }
        
        if (positiveCount == 0 || negativeCount == 0) {
            return 0.0; // No asymmetry if all movements are in one direction
        }
        
        double positiveAvg = positiveSum / positiveCount;
        double negativeAvg = negativeSum / negativeCount;
        
        // Calculate asymmetry ratio
        double maxAvg = Math.max(positiveAvg, negativeAvg);
        if (maxAvg < EPSILON) {
            return 0.0; // No asymmetry if both averages are effectively zero
        }
        return Math.abs(negativeAvg - positiveAvg) / maxAvg;
    }
    
    /**
     * Analyzes volatility in the financial time series.
     * 
     * <p>Uses wavelet analysis to measure volatility by analyzing the energy
     * in the detail coefficients, which capture high-frequency fluctuations.</p>
     * 
     * @param prices the price time series (must not be null)
     * @return the volatility measure; compare against configured thresholds for classification
     * @throws IllegalArgumentException if prices is null or invalid length
     */
    public double analyzeVolatility(double[] prices) {
        validatePrices(prices);
        
        // Convert prices to returns
        double[] returns = calculateReturns(prices);
        
        // Perform wavelet transform
        MODWTResult result = transform.forward(returns);
        double[] details = result.detailCoeffs();
        
        // Calculate energy in detail coefficients (volatility measure)
        double energy = 0.0;
        for (double detail : details) {
            energy += detail * detail;
        }
        
        return Math.sqrt(energy / details.length);
    }
    
    /**
     * Analyzes regime trends in the financial time series.
     * 
     * <p>Uses wavelet analysis to detect potential regime changes by analyzing
     * the trend in approximation coefficients over time.</p>
     * 
     * @param prices the price time series (must not be null)
     * @return the trend change measure; values above the configured threshold indicate regime shifts
     * @throws IllegalArgumentException if prices is null or invalid length
     */
    public double analyzeRegimeTrend(double[] prices) {
        validatePrices(prices);
        
        // Convert prices to returns
        double[] returns = calculateReturns(prices);
        
        // Perform wavelet transform
        MODWTResult result = transform.forward(returns);
        double[] approx = result.approximationCoeffs();
        
        if (approx.length < 2) {
            return 0.0;
        }
        
        // Calculate the maximum change in approximation coefficients
        double maxChange = 0.0;
        for (int i = 1; i < approx.length; i++) {
            double change = Math.abs(approx[i] - approx[i-1]);
            maxChange = Math.max(maxChange, change);
        }
        
        return maxChange;
    }
    
    /**
     * Detects anomalies in the financial time series.
     * 
     * <p>Uses statistical analysis of wavelet coefficients to identify
     * outliers that deviate significantly from normal behavior.</p>
     * 
     * @param prices the price time series (must not be null)
     * @return true if anomalies are detected above the configured threshold
     * @throws IllegalArgumentException if prices is null or invalid length
     */
    public boolean detectAnomalies(double[] prices) {
        validatePrices(prices);
        
        // Convert prices to returns
        double[] returns = calculateReturns(prices);
        
        // Perform wavelet transform
        MODWTResult result = transform.forward(returns);
        double[] details = result.detailCoeffs();
        
        // Calculate mean and standard deviation of detail coefficients
        double mean = 0.0;
        for (double detail : details) {
            mean += detail;
        }
        mean /= details.length;
        
        double variance = 0.0;
        for (double detail : details) {
            double diff = detail - mean;
            variance += diff * diff;
        }
        double stdDev = Math.sqrt(variance / details.length);
        
        // Check for outliers beyond configured threshold
        for (double detail : details) {
            if (Math.abs(detail - mean) > config.getAnomalyDetectionThreshold() * stdDev) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Classifies volatility as low, normal, or high based on configured thresholds.
     * 
     * @param volatility the volatility measure from {@link #analyzeVolatility(double[])}
     * @return the volatility classification
     */
    public VolatilityClassification classifyVolatility(double volatility) {
        if (volatility < config.getVolatilityLowThreshold()) {
            return VolatilityClassification.LOW;
        } else if (volatility > config.getVolatilityHighThreshold()) {
            return VolatilityClassification.HIGH;
        } else {
            return VolatilityClassification.NORMAL;
        }
    }
    
    /**
     * Checks if crash asymmetry exceeds the configured threshold.
     * 
     * @param asymmetry the asymmetry measure from {@link #analyzeCrashAsymmetry(double[])}
     * @return true if asymmetry indicates potential crash conditions
     */
    public boolean isCrashRisk(double asymmetry) {
        return asymmetry > config.getCrashAsymmetryThreshold();
    }
    
    /**
     * Checks if regime trend change exceeds the configured threshold.
     * 
     * @param trendChange the trend change measure from {@link #analyzeRegimeTrend(double[])}
     * @return true if trend change indicates potential regime shift
     */
    public boolean isRegimeShift(double trendChange) {
        return trendChange > config.getRegimeTrendThreshold();
    }
    
    /**
     * Gets the configuration used by this analyzer.
     * 
     * @return the analysis configuration
     */
    public FinancialAnalysisConfig getConfig() {
        return config;
    }
    
    /**
     * Converts price series to return series.
     */
    private double[] calculateReturns(double[] prices) {
        if (prices.length < 2) {
            return new double[0];
        }
        
        double[] returns = new double[prices.length - 1];
        for (int i = 1; i < prices.length; i++) {
            if (Math.abs(prices[i-1]) > EPSILON) {
                returns[i-1] = (prices[i] - prices[i-1]) / prices[i-1];
            } else {
                // Price is effectively zero, cannot calculate percentage return
                returns[i-1] = 0.0;
            }
        }
        
        // MODWT works with any signal length - no padding needed
        return returns;
    }
    
    /**
     * Validates the input price array.
     */
    private void validatePrices(double[] prices) {
        if (prices == null) {
            throw new IllegalArgumentException("Prices cannot be null");
        }
        if (prices.length < 2) {
            throw new IllegalArgumentException("Prices must contain at least 2 elements");
        }
        
        // Check for NaN or infinite values
        for (int i = 0; i < prices.length; i++) {
            if (!Double.isFinite(prices[i])) {
                throw new IllegalArgumentException("Prices must contain only finite values");
            }
        }
    }
    
    // nextPowerOfTwo method removed - MODWT doesn't need power-of-2 lengths
    
    /**
     * Enumeration for volatility classification.
     */
    public enum VolatilityClassification {
        LOW, NORMAL, HIGH
    }
}