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.
- wavqwise/__init__.py +27 -0
- wavqwise/alerts/__init__.py +0 -0
- wavqwise/anomaly/__init__.py +0 -0
- wavqwise/anomaly/dbscan_detector.py +6 -0
- wavqwise/anomaly/isolation_forest.py +25 -0
- wavqwise/anomaly/pipeline.py +49 -0
- wavqwise/anomaly/statistical.py +53 -0
- wavqwise/anomaly/stl_detector.py +7 -0
- wavqwise/cli/__init__.py +0 -0
- wavqwise/cli/main.py +52 -0
- wavqwise/core/__init__.py +4 -0
- wavqwise/core/base.py +129 -0
- wavqwise/core/config.py +54 -0
- wavqwise/core/incremental.py +72 -0
- wavqwise/core/pipeline.py +491 -0
- wavqwise/core/registry.py +271 -0
- wavqwise/database/__init__.py +0 -0
- wavqwise/database/influxdb.py +5 -0
- wavqwise/database/mongodb.py +5 -0
- wavqwise/database/mysql.py +2 -0
- wavqwise/database/postgres.py +14 -0
- wavqwise/database/sqlite.py +17 -0
- wavqwise/database/timescaledb.py +2 -0
- wavqwise/ecosystem/__init__.py +0 -0
- wavqwise/ecosystem/bridge.py +9 -0
- wavqwise/evaluation/__init__.py +0 -0
- wavqwise/evaluation/ab_test.py +21 -0
- wavqwise/evaluation/metrics.py +53 -0
- wavqwise/export/__init__.py +0 -0
- wavqwise/features/__init__.py +0 -0
- wavqwise/features/auto_features.py +34 -0
- wavqwise/forecasters/__init__.py +0 -0
- wavqwise/forecasters/cloud/__init__.py +0 -0
- wavqwise/forecasters/cloud/anthropic_wrapper.py +4 -0
- wavqwise/forecasters/cloud/ollama.py +7 -0
- wavqwise/forecasters/cloud/openai.py +6 -0
- wavqwise/forecasters/cloud/timegpt.py +4 -0
- wavqwise/forecasters/ensemble.py +2 -0
- wavqwise/forecasters/foundation/__init__.py +0 -0
- wavqwise/forecasters/foundation/chronos.py +37 -0
- wavqwise/forecasters/foundation/huggingface.py +7 -0
- wavqwise/forecasters/foundation/lagllama.py +4 -0
- wavqwise/forecasters/foundation/moirai.py +4 -0
- wavqwise/forecasters/foundation/timesfm.py +4 -0
- wavqwise/forecasters/ml/__init__.py +0 -0
- wavqwise/forecasters/ml/gradient_boost.py +26 -0
- wavqwise/forecasters/ml/linear.py +17 -0
- wavqwise/forecasters/ml/sklearn_wrapper.py +57 -0
- wavqwise/forecasters/model_selector.py +2 -0
- wavqwise/forecasters/neural/__init__.py +0 -0
- wavqwise/forecasters/neural/nbeats.py +4 -0
- wavqwise/forecasters/neural/neuralprophet_wrapper.py +31 -0
- wavqwise/forecasters/neural/tft.py +4 -0
- wavqwise/forecasters/traditional/__init__.py +0 -0
- wavqwise/forecasters/traditional/arima.py +79 -0
- wavqwise/forecasters/traditional/ces.py +4 -0
- wavqwise/forecasters/traditional/croston.py +4 -0
- wavqwise/forecasters/traditional/ets.py +51 -0
- wavqwise/forecasters/traditional/holtwinters.py +7 -0
- wavqwise/forecasters/traditional/moving_average.py +99 -0
- wavqwise/forecasters/traditional/naive.py +65 -0
- wavqwise/forecasters/traditional/sarima.py +44 -0
- wavqwise/forecasters/traditional/theta.py +31 -0
- wavqwise/loaders/__init__.py +0 -0
- wavqwise/loaders/auto_loader.py +27 -0
- wavqwise/loaders/base.py +8 -0
- wavqwise/preprocessors/__init__.py +0 -0
- wavqwise/preprocessors/auto_preprocessor.py +38 -0
- wavqwise/runtime/__init__.py +2 -0
- wavqwise/runtime/engine.py +334 -0
- wavqwise/runtime/onnx_inference.py +180 -0
- wavqwise/signals/__init__.py +0 -0
- wavqwise/signals/pipeline.py +102 -0
- wavqwise/trading/__init__.py +0 -0
- wavqwise/trading/indicators/__init__.py +0 -0
- wavqwise/trading/indicators/momentum.py +23 -0
- wavqwise/trading/indicators/trend.py +24 -0
- wavqwise/trading/indicators/volatility.py +24 -0
- wavqwise/trading/indicators/volume.py +5 -0
- wavqwise/trading/pipeline.py +50 -0
- wavqwise/visualization/__init__.py +0 -0
- wavqwise/visualization/anomaly_plot.py +26 -0
- wavqwise/visualization/dashboard.py +9 -0
- wavqwise/visualization/forecast_plot.py +54 -0
- wavqwise-0.1.0.dist-info/METADATA +417 -0
- wavqwise-0.1.0.dist-info/RECORD +89 -0
- wavqwise-0.1.0.dist-info/WHEEL +5 -0
- wavqwise-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|
wavqwise/cli/__init__.py
ADDED
|
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()
|
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
|
wavqwise/core/config.py
ADDED
|
@@ -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()
|