import Foundation class PredictionEngine { static let shared = PredictionEngine() private static let calendar = Calendar.current private init() {} // MARK: - Public Interface func predict(snapshots: [Snapshot], monthsAhead: Int = 12, algorithm: PredictionAlgorithm? = nil) -> PredictionResult { guard snapshots.count >= 3 else { return emptyResult() } let sorted = snapshots.sorted { $0.date < $1.date } return predictFromValues( sorted.map { $0.decimalValue.doubleValue }, dates: sorted.map { $0.date }, monthsAhead: monthsAhead, algorithm: algorithm ) } func predict(series: [(date: Date, value: Decimal)], monthsAhead: Int = 12, algorithm: PredictionAlgorithm? = nil) -> PredictionResult { guard series.count >= 3 else { return emptyResult() } let sorted = series.sorted { $0.date < $1.date } return predictFromValues( sorted.map { NSDecimalNumber(decimal: $0.value).doubleValue }, dates: sorted.map { $0.date }, monthsAhead: monthsAhead, algorithm: algorithm ) } // MARK: - Private Core private func emptyResult() -> PredictionResult { PredictionResult(predictions: [], algorithm: .linear, accuracy: 0, volatility: 0) } private func predictFromValues(_ values: [Double], dates: [Date], monthsAhead: Int, algorithm: PredictionAlgorithm?) -> PredictionResult { let volatility = calculateVolatility(values: values) let selectedAlgorithm = algorithm ?? selectBestAlgorithm(volatility: volatility) let lastDate = dates.last! let firstDate = dates.first! let predictions: [Prediction] let accuracy: Double switch selectedAlgorithm { case .linear: predictions = linearPredictions(values: values, firstDate: firstDate, lastDate: lastDate, monthsAhead: monthsAhead) accuracy = linearAccuracy(values: values, firstDate: firstDate) case .exponentialSmoothing: predictions = esPredictions(values: values, lastDate: lastDate, monthsAhead: monthsAhead) accuracy = esAccuracy(values: values) case .movingAverage: predictions = maPredictions(values: values, lastDate: lastDate, monthsAhead: monthsAhead) accuracy = maAccuracy(values: values) case .holtTrend: predictions = holtPredictions(values: values, lastDate: lastDate, monthsAhead: monthsAhead) accuracy = holtAccuracy(values: values) } return PredictionResult(predictions: predictions, algorithm: selectedAlgorithm, accuracy: accuracy, volatility: volatility) } // MARK: - Algorithm Selection private func selectBestAlgorithm(volatility: Double) -> PredictionAlgorithm { switch volatility { case 0..<8: return .holtTrend case 8..<20: return .exponentialSmoothing default: return .movingAverage } } // MARK: - Linear Regression private func linearPredictions(values: [Double], firstDate: Date, lastDate: Date, monthsAhead: Int) -> [Prediction] { let dataPoints = values.enumerated().map { (x: Double($0.offset), y: $0.element) } let (slope, intercept) = calculateLinearRegression(dataPoints: dataPoints) let residualStdDev = calculateResidualStdDev(dataPoints: dataPoints, slope: slope, intercept: intercept) let n = Double(values.count) return (1...monthsAhead).compactMap { month in guard let futureDate = Self.calendar.date(byAdding: .month, value: month, to: lastDate) else { return nil } let x = n - 1 + Double(month) let predicted = max(0, slope * x + intercept) let width = residualStdDev * 1.96 * (1.0 + Double(month) * 0.02) return Prediction(date: futureDate, predictedValue: Decimal(predicted), algorithm: .linear, confidenceInterval: .init(lower: Decimal(max(0, predicted - width)), upper: Decimal(predicted + width))) } } private func linearAccuracy(values: [Double], firstDate: Date) -> Double { guard values.count >= 5 else { return 0.5 } let splitIndex = Int(Double(values.count) * 0.8) let trainPoints = Array(values.prefix(splitIndex)).enumerated().map { (x: Double($0.offset), y: $0.element) } let (slope, intercept) = calculateLinearRegression(dataPoints: trainPoints) let validation = Array(values.suffix(from: splitIndex)) let mean = validation.reduce(0, +) / Double(validation.count) var ssRes = 0.0, ssTot = 0.0 for (i, actual) in validation.enumerated() { let x = Double(splitIndex + i) ssRes += pow(actual - (slope * x + intercept), 2) ssTot += pow(actual - mean, 2) } guard ssTot != 0 else { return 0.5 } return min(1.0, max(0, 1 - ssRes / ssTot)) } // MARK: - Exponential Smoothing private func esPredictions(values: [Double], lastDate: Date, monthsAhead: Int, alpha: Double = 0.3) -> [Prediction] { var smoothed = values[0] for i in 1..= 2 ? (values.suffix(3).reduce(0, +) / Double(min(3, values.count)) - values.prefix(3).reduce(0, +) / Double(min(3, values.count))) / Double(values.count) : 0 let stdDev = calculateStdDev(values: values) return (1...monthsAhead).compactMap { month in guard let futureDate = Self.calendar.date(byAdding: .month, value: month, to: lastDate) else { return nil } let predicted = max(0, smoothed + trend * Double(month)) let width = stdDev * 1.96 * (1.0 + Double(month) * 0.05) return Prediction(date: futureDate, predictedValue: Decimal(predicted), algorithm: .exponentialSmoothing, confidenceInterval: .init(lower: Decimal(max(0, predicted - width)), upper: Decimal(predicted + width))) } } private func esAccuracy(values: [Double]) -> Double { guard values.count >= 5 else { return 0.5 } let splitIndex = Int(Double(values.count) * 0.8) var smoothed = values[0] for i in 1.. [Prediction] { guard values.count >= windowSize else { return [] } let recent = Array(values.suffix(windowSize)) let movingAvg = recent.reduce(0, +) / Double(windowSize) var changes: [Double] = [] for i in 1.. Double { guard values.count >= 5 else { return 0.5 } let splitIndex = Int(Double(values.count) * 0.8) guard splitIndex > windowSize else { return 0.5 } let recentWindow = Array(values[(splitIndex - windowSize).. [Prediction] { var level = values[0] var trend = values[1] - values[0] var fitted: [Double] = [] for value in values { let lastLevel = level level = alpha * value + (1 - alpha) * (level + trend) trend = beta * (level - lastLevel) + (1 - beta) * trend fitted.append(level + trend) } let stdDev = calculateStdDev(values: zip(values, fitted).map { $0 - $1 }) return (1...monthsAhead).compactMap { month in guard let futureDate = Self.calendar.date(byAdding: .month, value: month, to: lastDate) else { return nil } let predicted = max(0, level + Double(month) * trend) let width = stdDev * 1.96 * (1.0 + Double(month) * 0.04) return Prediction(date: futureDate, predictedValue: Decimal(predicted), algorithm: .holtTrend, confidenceInterval: .init(lower: Decimal(max(0, predicted - width)), upper: Decimal(predicted + width))) } } private func holtAccuracy(values: [Double]) -> Double { guard values.count >= 5 else { return 0.5 } let splitIndex = Int(Double(values.count) * 0.8) guard splitIndex >= 2 else { return 0.5 } var level = values[0] var trend = values[1] - values[0] for value in values.prefix(splitIndex) { let lastLevel = level level = 0.4 * value + 0.6 * (level + trend) trend = 0.3 * (level - lastLevel) + 0.7 * trend } let validation = Array(values.suffix(from: splitIndex)) let mean = validation.reduce(0, +) / Double(validation.count) var ssRes = 0.0, ssTot = 0.0 for (i, actual) in validation.enumerated() { ssRes += pow(actual - (level + Double(i + 1) * trend), 2) ssTot += pow(actual - mean, 2) } guard ssTot != 0 else { return 0.5 } return max(0, min(1.0, 1 - ssRes / ssTot)) } // MARK: - Helpers private func calculateVolatility(values: [Double]) -> Double { guard values.count >= 2 else { return 0 } var returns: [Double] = [] for i in 1.. (slope: Double, intercept: Double) { let n = Double(dataPoints.count) let sumX = dataPoints.reduce(0) { $0 + $1.x } let sumY = dataPoints.reduce(0) { $0 + $1.y } let sumXY = dataPoints.reduce(0) { $0 + $1.x * $1.y } let sumX2 = dataPoints.reduce(0) { $0 + $1.x * $1.x } let denom = n * sumX2 - sumX * sumX guard denom != 0 else { return (0, sumY / n) } let slope = (n * sumXY - sumX * sumY) / denom return (slope, (sumY - slope * sumX) / n) } private func calculateResidualStdDev(dataPoints: [(x: Double, y: Double)], slope: Double, intercept: Double) -> Double { guard dataPoints.count > 2 else { return 0 } let mse = dataPoints.map { pow($0.y - (slope * $0.x + intercept), 2) }.reduce(0, +) / Double(dataPoints.count - 2) return sqrt(mse) } private func calculateStdDev(values: [Double]) -> Double { guard values.count >= 2 else { return 0 } let mean = values.reduce(0, +) / Double(values.count) let variance = values.map { pow($0 - mean, 2) }.reduce(0, +) / Double(values.count - 1) return sqrt(variance) } // MARK: - Public compatibility (kept for external callers) func predictLinear(snapshots: [Snapshot], monthsAhead: Int = 12) -> [Prediction] { guard snapshots.count >= 3 else { return [] } let sorted = snapshots.sorted { $0.date < $1.date } return linearPredictions( values: sorted.map { $0.decimalValue.doubleValue }, firstDate: sorted.first!.date, lastDate: sorted.last!.date, monthsAhead: monthsAhead ) } }