import { ARIMA } from "../models/arima"; interface ForecastResult { forecasts: number[]; confidenceIntervals: { lower: number[]; upper: number[] }; } export interface StepwiseForecastResult extends ForecastResult { updatedModels: ARIMA[]; errors: number[]; actualValues: number[]; } export interface RealTimeForecastResult extends ForecastResult { errors?: number[]; actualValues?: number[]; } export abstract class ForecastStrategy { protected baseModel: ARIMA; protected initialData: number[]; constructor(baseModel: ARIMA, initialData: number[]) { this.baseModel = baseModel; this.initialData = [...initialData]; } abstract forecast(steps: number): ForecastResult; abstract forecastWithRealTimeData(actualObservations: number[]): RealTimeForecastResult; abstract addObservationAndForecast(newObservation: number): { forecast: number; lowerBound: number; upperBound: number; error?: number }; } export class Stepwise extends ForecastStrategy { private refitModel: boolean; private verbose: boolean; private currentData: number[]; private currentModel: ARIMA; private lastForecast: { forecast: number; lowerBound: number; upperBound: number } | null = null; constructor( baseModel: ARIMA, initialData: number[], options: { refitModel?: boolean; verbose?: boolean; } = {} ) { super(baseModel, initialData); this.refitModel = options.refitModel ?? true; this.verbose = options.verbose ?? false; this.currentData = [...initialData]; this.currentModel = baseModel; } forecast(steps: number): ForecastResult { const result = this.currentModel.forecast(steps); return { forecasts: result.forecast, confidenceIntervals: { lower: result.lowerBound, upper: result.upperBound } }; } forecastWithRealTimeData(actualObservations: number[]): StepwiseForecastResult { const steps = actualObservations.length; const forecasts: number[] = []; const lowerBounds: number[] = []; const upperBounds: number[] = []; const updatedModels: ARIMA[] = []; const errors: number[] = []; let workingData = [...this.currentData]; let workingModel = this.currentModel; for (let step = 0; step < steps; step++) { const oneStepForecast = workingModel.forecast(1); const forecastValue = oneStepForecast.forecast[0]; const lowerBound = oneStepForecast.lowerBound[0]; const upperBound = oneStepForecast.upperBound[0]; forecasts.push(forecastValue); lowerBounds.push(lowerBound); upperBounds.push(upperBound); const actualObservation = actualObservations[step]; const error = Math.abs(forecastValue - actualObservation); errors.push(error); workingData.push(actualObservation); // refit model if enabled if (this.refitModel) { const updatedModel = new ARIMA(workingModel.getParams()); updatedModel.fit(workingData); workingModel = updatedModel; updatedModels.push(updatedModel); } if (this.verbose) { console.log(`Step ${step + 1}: Forecast=${forecastValue.toFixed(3)}, Actual=${actualObservation.toFixed(3)}, Error=${error.toFixed(3)}`); } } this.currentData = workingData; this.currentModel = workingModel; return { forecasts, confidenceIntervals: { lower: lowerBounds, upper: upperBounds }, updatedModels, errors, actualValues: actualObservations }; } addObservationAndForecast(newObservation: number): { forecast: number; lowerBound: number; upperBound: number; error?: number } { let error: number | undefined; if (this.lastForecast) { error = Math.abs(this.lastForecast.forecast - newObservation); if (this.verbose) { console.log(`Previous forecast: ${this.lastForecast.forecast.toFixed(3)}, Actual observation: ${newObservation.toFixed(3)}, Error: ${error.toFixed(3)}`); } } this.currentData.push(newObservation); if (this.refitModel) { const updatedModel = new ARIMA(this.currentModel.getParams()); updatedModel.fit(this.currentData); this.currentModel = updatedModel; } const nextForecast = this.currentModel.forecast(1); const forecastResult = { forecast: nextForecast.forecast[0], lowerBound: nextForecast.lowerBound[0], upperBound: nextForecast.upperBound[0] }; this.lastForecast = forecastResult; if (this.verbose) { console.log(`Next forecast: ${forecastResult.forecast.toFixed(3)}`); } return { ...forecastResult, error }; } reset(newInitialData: number[]): void { this.currentData = [...newInitialData]; this.currentModel = new ARIMA(this.baseModel.getParams()); this.currentModel.fit(this.currentData); this.lastForecast = null; } getCurrentData(): number[] { return [...this.currentData]; } getCurrentModel(): ARIMA { return this.currentModel; } } export class RollingWindow extends ForecastStrategy { private windowSize: number; private verbose: boolean; private currentData: number[]; private lastForecast: { forecast: number; lowerBound: number; upperBound: number } | null = null; constructor( baseModel: ARIMA, initialData: number[], options: { windowSize?: number; verbose?: boolean; } = {} ) { super(baseModel, initialData); this.windowSize = options.windowSize ?? initialData.length; this.verbose = options.verbose ?? false; this.currentData = [...initialData]; } forecast(steps: number): ForecastResult { // Use current window to forecast multiple steps ahead const windowData = this.currentData.slice(-this.windowSize); const model = new ARIMA(this.baseModel.getParams()); model.fit(windowData); const result = model.forecast(steps); return { forecasts: result.forecast, confidenceIntervals: { lower: result.lowerBound, upper: result.upperBound } }; } forecastWithRealTimeData(actualObservations: number[]): RealTimeForecastResult { const steps = actualObservations.length; const forecasts: number[] = []; const lowerBounds: number[] = []; const upperBounds: number[] = []; const errors: number[] = []; let workingData = [...this.currentData]; for (let step = 0; step < steps; step++) { // Keep only the most recent observations (rolling window) const windowData = workingData.slice(-this.windowSize); // Fit model on current window const model = new ARIMA(this.baseModel.getParams()); model.fit(windowData); // Forecast one step ahead const oneStepForecast = model.forecast(1); const forecastValue = oneStepForecast.forecast[0]; const lowerBound = oneStepForecast.lowerBound[0]; const upperBound = oneStepForecast.upperBound[0]; // Store results forecasts.push(forecastValue); lowerBounds.push(lowerBound); upperBounds.push(upperBound); // Use actual observation and calculate error const actualObservation = actualObservations[step]; const error = Math.abs(forecastValue - actualObservation); errors.push(error); workingData.push(actualObservation); if (this.verbose) { console.log(`Rolling Step ${step + 1}: Forecast=${forecastValue.toFixed(3)}, Actual=${actualObservation.toFixed(3)}, Error=${error.toFixed(3)}, Window Size=${windowData.length}`); } } // Update internal state this.currentData = workingData; return { forecasts, confidenceIntervals: { lower: lowerBounds, upper: upperBounds }, errors, actualValues: actualObservations }; } addObservationAndForecast(newObservation: number): { forecast: number; lowerBound: number; upperBound: number; error?: number } { let error: number | undefined; // Calculate error if we have a previous forecast if (this.lastForecast) { error = Math.abs(this.lastForecast.forecast - newObservation); if (this.verbose) { console.log(`Previous forecast: ${this.lastForecast.forecast.toFixed(3)}, Actual observation: ${newObservation.toFixed(3)}, Error: ${error.toFixed(3)}`); } } // Add new observation this.currentData.push(newObservation); // Keep only the most recent observations (rolling window) const windowData = this.currentData.slice(-this.windowSize); // Fit model on current window const model = new ARIMA(this.baseModel.getParams()); model.fit(windowData); // Get next forecast const nextForecast = model.forecast(1); const forecastResult = { forecast: nextForecast.forecast[0], lowerBound: nextForecast.lowerBound[0], upperBound: nextForecast.upperBound[0] }; // Store this forecast for next error calculation this.lastForecast = forecastResult; if (this.verbose) { console.log(`Next forecast: ${forecastResult.forecast.toFixed(3)}, Window Size: ${windowData.length}`); } return { ...forecastResult, error }; } setWindowSize(newWindowSize: number): void { this.windowSize = newWindowSize; } getWindowSize(): number { return this.windowSize; } getCurrentData(): number[] { return [...this.currentData]; } reset(newInitialData: number[]): void { this.currentData = [...newInitialData]; this.lastForecast = null; } } export class Adaptive extends ForecastStrategy { private stepwiseStrategy: Stepwise; private rollingStrategy: RollingWindow; private adaptationThreshold: number; private errorWindow: number[]; private maxErrorWindowSize: number; private currentStrategy: 'stepwise' | 'rolling'; private verbose: boolean; constructor( baseModel: ARIMA, initialData: number[], options: { windowSize?: number; adaptationThreshold?: number; maxErrorWindowSize?: number; verbose?: boolean; } = {} ) { super(baseModel, initialData); this.stepwiseStrategy = new Stepwise(baseModel, initialData, { refitModel: true, verbose: options.verbose }); this.rollingStrategy = new RollingWindow(baseModel, initialData, { windowSize: options.windowSize, verbose: options.verbose }); this.adaptationThreshold = options.adaptationThreshold ?? 2.0; this.maxErrorWindowSize = options.maxErrorWindowSize ?? 10; this.errorWindow = []; this.currentStrategy = 'stepwise'; this.verbose = options.verbose ?? false; } forecast(steps: number): ForecastResult { if (this.currentStrategy === 'stepwise') { return this.stepwiseStrategy.forecast(steps); } else { return this.rollingStrategy.forecast(steps); } } forecastWithRealTimeData(actualObservations: number[]): RealTimeForecastResult { const results = this.currentStrategy === 'stepwise' ? this.stepwiseStrategy.forecastWithRealTimeData(actualObservations) : this.rollingStrategy.forecastWithRealTimeData(actualObservations); if (results.errors) { results.errors.forEach(error => this.addError(error)); if (this.shouldSwitchStrategy()) { this.switchStrategy(); } } return results; } addObservationAndForecast(newObservation: number): { forecast: number; lowerBound: number; upperBound: number; error?: number } { const result = this.currentStrategy === 'stepwise' ? this.stepwiseStrategy.addObservationAndForecast(newObservation) : this.rollingStrategy.addObservationAndForecast(newObservation); if (result.error !== undefined) { this.addError(result.error); if (this.shouldSwitchStrategy()) { this.switchStrategy(); } } if (this.verbose) { const errorText = result.error !== undefined ? `, Error: ${result.error.toFixed(3)}` : ''; console.log(`Adaptive strategy (${this.currentStrategy}): New observation: ${newObservation.toFixed(3)}, Next forecast: ${result.forecast.toFixed(3)}${errorText}`); } return result; } private shouldSwitchStrategy(): boolean { if (this.errorWindow.length < this.maxErrorWindowSize) { return false; } const recentAvgError = this.errorWindow.slice(-5).reduce((a, b) => a + b, 0) / 5; return recentAvgError > this.adaptationThreshold; } private addError(error: number): void { this.errorWindow.push(error); if (this.errorWindow.length > this.maxErrorWindowSize) { this.errorWindow.shift(); } } private switchStrategy(): void { const oldStrategy = this.currentStrategy; this.currentStrategy = this.currentStrategy === 'stepwise' ? 'rolling' : 'stepwise'; if (this.verbose) { console.log(`Switching strategy from ${oldStrategy} to ${this.currentStrategy}`); } this.errorWindow = []; } getCurrentStrategy(): 'stepwise' | 'rolling' { return this.currentStrategy; } reset(newInitialData: number[]): void { this.stepwiseStrategy.reset(newInitialData); this.rollingStrategy.reset(newInitialData); this.errorWindow = []; this.currentStrategy = 'stepwise'; } }