wavqwise 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. wavqwise/__init__.py +27 -0
  2. wavqwise/alerts/__init__.py +0 -0
  3. wavqwise/anomaly/__init__.py +0 -0
  4. wavqwise/anomaly/dbscan_detector.py +6 -0
  5. wavqwise/anomaly/isolation_forest.py +25 -0
  6. wavqwise/anomaly/pipeline.py +49 -0
  7. wavqwise/anomaly/statistical.py +53 -0
  8. wavqwise/anomaly/stl_detector.py +7 -0
  9. wavqwise/cli/__init__.py +0 -0
  10. wavqwise/cli/main.py +52 -0
  11. wavqwise/core/__init__.py +4 -0
  12. wavqwise/core/base.py +129 -0
  13. wavqwise/core/config.py +54 -0
  14. wavqwise/core/incremental.py +72 -0
  15. wavqwise/core/pipeline.py +491 -0
  16. wavqwise/core/registry.py +271 -0
  17. wavqwise/database/__init__.py +0 -0
  18. wavqwise/database/influxdb.py +5 -0
  19. wavqwise/database/mongodb.py +5 -0
  20. wavqwise/database/mysql.py +2 -0
  21. wavqwise/database/postgres.py +14 -0
  22. wavqwise/database/sqlite.py +17 -0
  23. wavqwise/database/timescaledb.py +2 -0
  24. wavqwise/ecosystem/__init__.py +0 -0
  25. wavqwise/ecosystem/bridge.py +9 -0
  26. wavqwise/evaluation/__init__.py +0 -0
  27. wavqwise/evaluation/ab_test.py +21 -0
  28. wavqwise/evaluation/metrics.py +53 -0
  29. wavqwise/export/__init__.py +0 -0
  30. wavqwise/features/__init__.py +0 -0
  31. wavqwise/features/auto_features.py +34 -0
  32. wavqwise/forecasters/__init__.py +0 -0
  33. wavqwise/forecasters/cloud/__init__.py +0 -0
  34. wavqwise/forecasters/cloud/anthropic_wrapper.py +4 -0
  35. wavqwise/forecasters/cloud/ollama.py +7 -0
  36. wavqwise/forecasters/cloud/openai.py +6 -0
  37. wavqwise/forecasters/cloud/timegpt.py +4 -0
  38. wavqwise/forecasters/ensemble.py +2 -0
  39. wavqwise/forecasters/foundation/__init__.py +0 -0
  40. wavqwise/forecasters/foundation/chronos.py +37 -0
  41. wavqwise/forecasters/foundation/huggingface.py +7 -0
  42. wavqwise/forecasters/foundation/lagllama.py +4 -0
  43. wavqwise/forecasters/foundation/moirai.py +4 -0
  44. wavqwise/forecasters/foundation/timesfm.py +4 -0
  45. wavqwise/forecasters/ml/__init__.py +0 -0
  46. wavqwise/forecasters/ml/gradient_boost.py +26 -0
  47. wavqwise/forecasters/ml/linear.py +17 -0
  48. wavqwise/forecasters/ml/sklearn_wrapper.py +57 -0
  49. wavqwise/forecasters/model_selector.py +2 -0
  50. wavqwise/forecasters/neural/__init__.py +0 -0
  51. wavqwise/forecasters/neural/nbeats.py +4 -0
  52. wavqwise/forecasters/neural/neuralprophet_wrapper.py +31 -0
  53. wavqwise/forecasters/neural/tft.py +4 -0
  54. wavqwise/forecasters/traditional/__init__.py +0 -0
  55. wavqwise/forecasters/traditional/arima.py +79 -0
  56. wavqwise/forecasters/traditional/ces.py +4 -0
  57. wavqwise/forecasters/traditional/croston.py +4 -0
  58. wavqwise/forecasters/traditional/ets.py +51 -0
  59. wavqwise/forecasters/traditional/holtwinters.py +7 -0
  60. wavqwise/forecasters/traditional/moving_average.py +99 -0
  61. wavqwise/forecasters/traditional/naive.py +65 -0
  62. wavqwise/forecasters/traditional/sarima.py +44 -0
  63. wavqwise/forecasters/traditional/theta.py +31 -0
  64. wavqwise/loaders/__init__.py +0 -0
  65. wavqwise/loaders/auto_loader.py +27 -0
  66. wavqwise/loaders/base.py +8 -0
  67. wavqwise/preprocessors/__init__.py +0 -0
  68. wavqwise/preprocessors/auto_preprocessor.py +38 -0
  69. wavqwise/runtime/__init__.py +2 -0
  70. wavqwise/runtime/engine.py +334 -0
  71. wavqwise/runtime/onnx_inference.py +180 -0
  72. wavqwise/signals/__init__.py +0 -0
  73. wavqwise/signals/pipeline.py +102 -0
  74. wavqwise/trading/__init__.py +0 -0
  75. wavqwise/trading/indicators/__init__.py +0 -0
  76. wavqwise/trading/indicators/momentum.py +23 -0
  77. wavqwise/trading/indicators/trend.py +24 -0
  78. wavqwise/trading/indicators/volatility.py +24 -0
  79. wavqwise/trading/indicators/volume.py +5 -0
  80. wavqwise/trading/pipeline.py +50 -0
  81. wavqwise/visualization/__init__.py +0 -0
  82. wavqwise/visualization/anomaly_plot.py +26 -0
  83. wavqwise/visualization/dashboard.py +9 -0
  84. wavqwise/visualization/forecast_plot.py +54 -0
  85. wavqwise-0.1.0.dist-info/METADATA +417 -0
  86. wavqwise-0.1.0.dist-info/RECORD +89 -0
  87. wavqwise-0.1.0.dist-info/WHEEL +5 -0
  88. wavqwise-0.1.0.dist-info/entry_points.txt +2 -0
  89. wavqwise-0.1.0.dist-info/top_level.txt +1 -0
wavqwise/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ WavqWise - Pluggable Temporal Intelligence
3
+ Sense. Forecast. Alert.
4
+
5
+ Any model. Any signal. Five lines to forecast.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "VK-Ant (Venkatkumar Rajan)"
10
+ __tagline__ = "Sense. Forecast. Alert."
11
+
12
+ from wavqwise.core.pipeline import WavqPipeline
13
+ from wavqwise.anomaly.pipeline import AnomalyPipeline
14
+ from wavqwise.trading.pipeline import TradingPipeline
15
+ from wavqwise.signals.pipeline import SignalPipeline
16
+ from wavqwise.ecosystem.bridge import EcosystemBridge
17
+ from wavqwise.core.registry import Registry
18
+
19
+ __all__ = [
20
+ "WavqPipeline",
21
+ "AnomalyPipeline",
22
+ "TradingPipeline",
23
+ "SignalPipeline",
24
+ "EcosystemBridge",
25
+ "Registry",
26
+ "__version__",
27
+ ]
File without changes
File without changes
@@ -0,0 +1,6 @@
1
+ from wavqwise.core.base import BaseAnomalyDetector
2
+ class DBSCANDetector(BaseAnomalyDetector):
3
+ def fit(self, data, **kwargs): pass
4
+ def detect(self, data=None, **kwargs):
5
+ from wavqwise.anomaly.statistical import ZScoreDetector
6
+ d = ZScoreDetector(); d.fit(data, kwargs.get("target", "value")); return d.detect(data)
@@ -0,0 +1,25 @@
1
+ """Isolation Forest anomaly detector."""
2
+ import numpy as np
3
+ import pandas as pd
4
+ from wavqwise.core.base import BaseAnomalyDetector
5
+
6
+ class IsolationForestDetector(BaseAnomalyDetector):
7
+ def __init__(self, contamination=0.05, **kwargs):
8
+ self.contamination = contamination
9
+ self._model = None
10
+ self._target = None
11
+
12
+ def fit(self, data, target="value", **kwargs):
13
+ from sklearn.ensemble import IsolationForest
14
+ self._target = target
15
+ self._model = IsolationForest(contamination=self.contamination, random_state=42)
16
+ self._model.fit(data[[target]])
17
+
18
+ def detect(self, data=None, **kwargs):
19
+ scores = self._model.decision_function(data[[self._target]])
20
+ preds = self._model.predict(data[[self._target]])
21
+ result = data.copy()
22
+ result["is_anomaly"] = preds == -1
23
+ result["anomaly_score"] = -scores
24
+ result["severity"] = np.where(-scores > 0.3, "critical", np.where(-scores > 0.2, "high", np.where(-scores > 0.1, "medium", "low")))
25
+ return result
@@ -0,0 +1,49 @@
1
+ """Anomaly detection pipeline."""
2
+ import pandas as pd
3
+ from wavqwise.core.registry import Registry
4
+ from wavqwise.loaders.auto_loader import AutoLoader
5
+
6
+ class AnomalyResult:
7
+ def __init__(self, data, target, time_col, method):
8
+ self.data = data
9
+ self.target = target
10
+ self.time_col = time_col
11
+ self.method = method
12
+ self.anomalies = data[data["is_anomaly"]]
13
+
14
+ def plot(self, engine="matplotlib", show_severity=False, **kwargs):
15
+ from wavqwise.visualization.anomaly_plot import AnomalyPlot
16
+ plotter = AnomalyPlot(engine=engine)
17
+ return plotter.plot(self.data, self.target, self.time_col, show_severity=show_severity)
18
+
19
+ def summary(self):
20
+ total = len(self.data)
21
+ n_anomalies = len(self.anomalies)
22
+ return f"Anomalies: {n_anomalies}/{total} ({n_anomalies/total*100:.1f}%) | Method: {self.method}"
23
+
24
+ def __repr__(self):
25
+ return f"AnomalyResult(anomalies={len(self.anomalies)}, method='{self.method}')"
26
+
27
+
28
+ class AnomalyPipeline:
29
+ def __init__(self):
30
+ self._data = None
31
+ self._target = None
32
+ self._time_col = None
33
+
34
+ def load(self, source, target=None, time=None, **kwargs):
35
+ if isinstance(source, pd.DataFrame):
36
+ self._data = source.copy()
37
+ else:
38
+ self._data = AutoLoader().load(source, **kwargs)
39
+ self._target = target or self._data.select_dtypes("number").columns[0]
40
+ self._time_col = time or self._data.columns[0]
41
+ if not pd.api.types.is_datetime64_any_dtype(self._data[self._time_col]):
42
+ self._data[self._time_col] = pd.to_datetime(self._data[self._time_col])
43
+ return self
44
+
45
+ def detect(self, method="zscore", **kwargs):
46
+ detector = Registry.get_anomaly_detector(method)
47
+ detector.fit(self._data, target=self._target)
48
+ result = detector.detect(self._data)
49
+ return AnomalyResult(result, self._target, self._time_col, method)
@@ -0,0 +1,53 @@
1
+ """Statistical anomaly detectors."""
2
+ import numpy as np
3
+ import pandas as pd
4
+ from wavqwise.core.base import BaseAnomalyDetector
5
+
6
+
7
+ class ZScoreDetector(BaseAnomalyDetector):
8
+ def __init__(self, threshold=3.0, **kwargs):
9
+ self.threshold = threshold
10
+ self._mean = self._std = self._target = None
11
+
12
+ def fit(self, data, target="value", **kwargs):
13
+ self._target = target
14
+ self._mean = data[target].mean()
15
+ self._std = data[target].std()
16
+
17
+ def detect(self, data=None, **kwargs):
18
+ vals = data[self._target]
19
+ scores = np.abs((vals - self._mean) / max(self._std, 1e-8))
20
+ is_anomaly = scores > self.threshold
21
+ severity = np.where(scores > self.threshold * 2, "critical",
22
+ np.where(scores > self.threshold * 1.5, "high",
23
+ np.where(scores > self.threshold, "medium", "low")))
24
+ result = data.copy()
25
+ result["is_anomaly"] = is_anomaly
26
+ result["anomaly_score"] = scores
27
+ result["severity"] = severity
28
+ return result
29
+
30
+
31
+ class IQRDetector(BaseAnomalyDetector):
32
+ def __init__(self, multiplier=1.5, **kwargs):
33
+ self.multiplier = multiplier
34
+ self._q1 = self._q3 = self._iqr = self._target = None
35
+
36
+ def fit(self, data, target="value", **kwargs):
37
+ self._target = target
38
+ self._q1 = data[target].quantile(0.25)
39
+ self._q3 = data[target].quantile(0.75)
40
+ self._iqr = self._q3 - self._q1
41
+
42
+ def detect(self, data=None, **kwargs):
43
+ vals = data[self._target]
44
+ lower = self._q1 - self.multiplier * self._iqr
45
+ upper = self._q3 + self.multiplier * self._iqr
46
+ is_anomaly = (vals < lower) | (vals > upper)
47
+ scores = np.where(vals < lower, (lower - vals) / max(self._iqr, 1e-8),
48
+ np.where(vals > upper, (vals - upper) / max(self._iqr, 1e-8), 0))
49
+ result = data.copy()
50
+ result["is_anomaly"] = is_anomaly
51
+ result["anomaly_score"] = scores
52
+ result["severity"] = np.where(scores > 3, "critical", np.where(scores > 2, "high", np.where(scores > 1, "medium", "low")))
53
+ return result
@@ -0,0 +1,7 @@
1
+ """STL decomposition anomaly detector."""
2
+ from wavqwise.core.base import BaseAnomalyDetector
3
+ class STLDetector(BaseAnomalyDetector):
4
+ def fit(self, data, **kwargs): self._target = kwargs.get("target", "value")
5
+ def detect(self, data=None, **kwargs):
6
+ from wavqwise.anomaly.statistical import ZScoreDetector
7
+ d = ZScoreDetector(); d.fit(data, self._target); return d.detect(data)
File without changes
wavqwise/cli/main.py ADDED
@@ -0,0 +1,52 @@
1
+ """CLI: wavqwise forecast --input data.csv --model arima --horizon 30"""
2
+ import argparse
3
+ import sys
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(prog="wavqwise", description="WavqWise - Sense. Forecast. Alert.")
7
+ sub = parser.add_subparsers(dest="command")
8
+
9
+ fc = sub.add_parser("forecast", help="Run forecasting pipeline")
10
+ fc.add_argument("--input", "-i", required=True)
11
+ fc.add_argument("--target", "-t", default=None)
12
+ fc.add_argument("--time", default=None)
13
+ fc.add_argument("--model", "-m", default="moving_average")
14
+ fc.add_argument("--horizon", "-H", type=int, default=30)
15
+ fc.add_argument("--plot", action="store_true")
16
+
17
+ det = sub.add_parser("detect", help="Run anomaly detection")
18
+ det.add_argument("--input", "-i", required=True)
19
+ det.add_argument("--target", "-t", default=None)
20
+ det.add_argument("--method", "-m", default="zscore")
21
+
22
+ mod = sub.add_parser("models", help="List available models")
23
+
24
+ args = parser.parse_args()
25
+
26
+ if args.command == "forecast":
27
+ from wavqwise import WavqPipeline
28
+ p = WavqPipeline()
29
+ p.load(args.input, target=args.target, time=args.time)
30
+ result = p.forecast(horizon=args.horizon, model=args.model)
31
+ print(result.summary())
32
+ if args.plot:
33
+ result.plot()
34
+
35
+ elif args.command == "detect":
36
+ from wavqwise import AnomalyPipeline
37
+ p = AnomalyPipeline()
38
+ p.load(args.input, target=args.target)
39
+ result = p.detect(method=args.method)
40
+ print(result.summary())
41
+
42
+ elif args.command == "models":
43
+ from wavqwise import WavqPipeline
44
+ print("Available models:")
45
+ for m in WavqPipeline.available_models():
46
+ print(f" - {m}")
47
+
48
+ else:
49
+ parser.print_help()
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,4 @@
1
+ from wavqwise.core.pipeline import WavqPipeline
2
+ from wavqwise.core.registry import Registry
3
+ from wavqwise.core.config import PipelineConfig
4
+ from wavqwise.core.incremental import IncrementalEngine
wavqwise/core/base.py ADDED
@@ -0,0 +1,129 @@
1
+ """Base interfaces for all pluggable components."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, List, Optional, Union
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+
9
+ class BaseForecaster(ABC):
10
+ """Base interface for all forecasting backends.
11
+ Every model - ARIMA, XGBoost, Chronos, Ollama - implements this.
12
+ User never sees this class. WavqPipeline uses it internally.
13
+ """
14
+
15
+ def __init__(self, **kwargs):
16
+ self._fitted = False
17
+ self._params = kwargs
18
+
19
+ @abstractmethod
20
+ def fit(self, data: pd.DataFrame, target: str = "value",
21
+ time_col: str = "timestamp", **kwargs):
22
+ """Initial training on historical data."""
23
+ pass
24
+
25
+ @abstractmethod
26
+ def predict(self, horizon: int = 30,
27
+ confidence_level: float = 0.95, **kwargs) -> pd.DataFrame:
28
+ """Generate forecast. Returns DataFrame with columns:
29
+ [time_col, target, target_lower, target_upper]
30
+ """
31
+ pass
32
+
33
+ def update(self, new_data: pd.DataFrame, **kwargs):
34
+ """Incremental update. Override for true incremental learning.
35
+ Default: raises NotImplementedError (triggers full refit in IncrementalEngine).
36
+ """
37
+ raise NotImplementedError("This model does not support incremental update.")
38
+
39
+ def save(self, path: str):
40
+ """Persist trained state."""
41
+ import joblib
42
+ joblib.dump(self, path)
43
+
44
+ @classmethod
45
+ def load(cls, path: str) -> "BaseForecaster":
46
+ """Load previously trained state."""
47
+ import joblib
48
+ return joblib.load(path)
49
+
50
+
51
+ class BasePreprocessor(ABC):
52
+ @abstractmethod
53
+ def transform(self, data: pd.DataFrame, target: str, time_col: str,
54
+ **kwargs) -> pd.DataFrame:
55
+ pass
56
+
57
+ def fit_transform(self, data: pd.DataFrame, target: str, time_col: str,
58
+ **kwargs) -> pd.DataFrame:
59
+ return self.transform(data, target, time_col, **kwargs)
60
+
61
+
62
+ class BaseFeatureEngineer(ABC):
63
+ @abstractmethod
64
+ def generate(self, data: pd.DataFrame, target: str, time_col: str,
65
+ **kwargs) -> pd.DataFrame:
66
+ pass
67
+
68
+
69
+ class BaseAnomalyDetector(ABC):
70
+ @abstractmethod
71
+ def fit(self, data: pd.DataFrame, target: str, **kwargs):
72
+ pass
73
+
74
+ @abstractmethod
75
+ def detect(self, data: Optional[pd.DataFrame] = None, **kwargs) -> pd.DataFrame:
76
+ """Returns DataFrame with columns: [timestamp, value, is_anomaly, severity, score]"""
77
+ pass
78
+
79
+
80
+ class BaseAlertHandler(ABC):
81
+ @abstractmethod
82
+ def send(self, anomaly: Dict[str, Any], **kwargs):
83
+ pass
84
+
85
+
86
+ class BaseLoader(ABC):
87
+ @abstractmethod
88
+ def load(self, source: str, **kwargs) -> pd.DataFrame:
89
+ pass
90
+
91
+
92
+ class BaseDBConnector(ABC):
93
+ @abstractmethod
94
+ def load(self, **kwargs) -> pd.DataFrame:
95
+ pass
96
+
97
+ @abstractmethod
98
+ def save(self, data: pd.DataFrame, **kwargs):
99
+ pass
100
+
101
+
102
+ class BaseIndicator(ABC):
103
+ @abstractmethod
104
+ def compute(self, data: pd.DataFrame, **kwargs) -> pd.DataFrame:
105
+ pass
106
+
107
+
108
+ class BaseStrategy(ABC):
109
+ @abstractmethod
110
+ def generate_signals(self, data: pd.DataFrame, **kwargs) -> pd.DataFrame:
111
+ pass
112
+
113
+
114
+ class BaseSignalProcessor(ABC):
115
+ @abstractmethod
116
+ def process(self, data: np.ndarray, sample_rate: float, **kwargs) -> Dict[str, Any]:
117
+ pass
118
+
119
+
120
+ class BaseEvaluator(ABC):
121
+ @abstractmethod
122
+ def evaluate(self, actual: np.ndarray, predicted: np.ndarray, **kwargs) -> Dict[str, float]:
123
+ pass
124
+
125
+
126
+ class BaseVisualizer(ABC):
127
+ @abstractmethod
128
+ def plot(self, **kwargs):
129
+ pass
@@ -0,0 +1,54 @@
1
+ """Pipeline configuration management."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List, Optional
5
+
6
+
7
+ @dataclass
8
+ class PipelineConfig:
9
+ """Configuration for WavqPipeline."""
10
+
11
+ model: str = "moving_average"
12
+ horizon: int = 30
13
+ target: Optional[str] = None
14
+ time_col: Optional[str] = None
15
+ frequency: Optional[str] = None # Auto-detected if None
16
+ confidence_level: float = 0.95
17
+ auto_preprocess: bool = True
18
+ auto_features: bool = True
19
+ incremental: bool = True
20
+ plot_engine: str = "matplotlib" # "matplotlib" or "plotly"
21
+ plot_style: str = "default"
22
+ verbose: bool = True
23
+ random_state: int = 42
24
+ n_jobs: int = -1
25
+ extra: Dict[str, Any] = field(default_factory=dict)
26
+
27
+
28
+ @dataclass
29
+ class AnomalyConfig:
30
+ """Configuration for AnomalyPipeline."""
31
+
32
+ method: str = "auto" # "zscore", "iqr", "isolation_forest", "stl", "auto"
33
+ sensitivity: float = 0.95
34
+ window_size: Optional[int] = None
35
+ streaming: bool = False
36
+ alert_handlers: List[str] = field(default_factory=list)
37
+ severity_levels: Dict[str, float] = field(default_factory=lambda: {
38
+ "low": 2.0,
39
+ "medium": 3.0,
40
+ "high": 4.0,
41
+ "critical": 5.0,
42
+ })
43
+
44
+
45
+ @dataclass
46
+ class TradingConfig:
47
+ """Configuration for TradingPipeline."""
48
+
49
+ indicators: List[str] = field(default_factory=list)
50
+ strategy: str = "momentum"
51
+ initial_capital: float = 100000.0
52
+ commission: float = 0.001
53
+ slippage: float = 0.0005
54
+ position_size: float = 1.0
@@ -0,0 +1,72 @@
1
+ """
2
+ Incremental training engine.
3
+ Models update with new data WITHOUT full retraining.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Optional
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class IncrementalEngine:
16
+ """Manages incremental model updates.
17
+
18
+ Strategy varies by model type:
19
+ - Statistical (ARIMA, ETS): append observations, update parameters
20
+ - ML (XGBoost, RF): warm-start or partial_fit
21
+ - Neural: fine-tune on new batch
22
+ - Foundation: zero-shot, no training needed
23
+ - Cloud/LLM: prompt with latest data
24
+ """
25
+
26
+ def __init__(self, window_size: Optional[int] = None):
27
+ self._history = None
28
+ self._window_size = window_size
29
+ self._update_count = 0
30
+
31
+ @property
32
+ def history(self) -> Optional[pd.DataFrame]:
33
+ return self._history
34
+
35
+ @property
36
+ def update_count(self) -> int:
37
+ return self._update_count
38
+
39
+ def initialize(self, data: pd.DataFrame) -> pd.DataFrame:
40
+ self._history = data.copy()
41
+ self._update_count = 0
42
+ return self._history
43
+
44
+ def update(self, new_data: pd.DataFrame, forecaster: Any) -> pd.DataFrame:
45
+ if self._history is None:
46
+ raise ValueError("Engine not initialized. Call initialize() first.")
47
+
48
+ self._history = pd.concat([self._history, new_data], ignore_index=True)
49
+
50
+ if self._window_size and len(self._history) > self._window_size:
51
+ self._history = self._history.tail(self._window_size).reset_index(drop=True)
52
+
53
+ if hasattr(forecaster, "update"):
54
+ try:
55
+ forecaster.update(new_data)
56
+ logger.info(
57
+ f"Incremental update #{self._update_count + 1}: "
58
+ f"{len(new_data)} new observations"
59
+ )
60
+ except NotImplementedError:
61
+ logger.info("Model does not support incremental update, refitting on window")
62
+ forecaster.fit(self._history)
63
+ else:
64
+ forecaster.fit(self._history)
65
+
66
+ self._update_count += 1
67
+ return self._history
68
+
69
+ def get_training_data(self) -> pd.DataFrame:
70
+ if self._history is None:
71
+ raise ValueError("Engine not initialized.")
72
+ return self._history.copy()