quantmllibrary 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.
- quantml/__init__.py +74 -0
- quantml/autograd.py +154 -0
- quantml/cli/__init__.py +10 -0
- quantml/cli/run_experiment.py +385 -0
- quantml/config/__init__.py +28 -0
- quantml/config/config.py +259 -0
- quantml/data/__init__.py +33 -0
- quantml/data/cache.py +149 -0
- quantml/data/feature_store.py +234 -0
- quantml/data/futures.py +254 -0
- quantml/data/loaders.py +236 -0
- quantml/data/memory_optimizer.py +234 -0
- quantml/data/validators.py +390 -0
- quantml/experiments/__init__.py +23 -0
- quantml/experiments/logger.py +208 -0
- quantml/experiments/results.py +158 -0
- quantml/experiments/tracker.py +223 -0
- quantml/features/__init__.py +25 -0
- quantml/features/base.py +104 -0
- quantml/features/gap_features.py +124 -0
- quantml/features/registry.py +138 -0
- quantml/features/volatility_features.py +140 -0
- quantml/features/volume_features.py +142 -0
- quantml/functional.py +37 -0
- quantml/models/__init__.py +27 -0
- quantml/models/attention.py +258 -0
- quantml/models/dropout.py +130 -0
- quantml/models/gru.py +319 -0
- quantml/models/linear.py +112 -0
- quantml/models/lstm.py +353 -0
- quantml/models/mlp.py +286 -0
- quantml/models/normalization.py +289 -0
- quantml/models/rnn.py +154 -0
- quantml/models/tcn.py +238 -0
- quantml/online.py +209 -0
- quantml/ops.py +1707 -0
- quantml/optim/__init__.py +42 -0
- quantml/optim/adafactor.py +206 -0
- quantml/optim/adagrad.py +157 -0
- quantml/optim/adam.py +267 -0
- quantml/optim/lookahead.py +97 -0
- quantml/optim/quant_optimizer.py +228 -0
- quantml/optim/radam.py +192 -0
- quantml/optim/rmsprop.py +203 -0
- quantml/optim/schedulers.py +286 -0
- quantml/optim/sgd.py +181 -0
- quantml/py.typed +0 -0
- quantml/streaming.py +175 -0
- quantml/tensor.py +462 -0
- quantml/time_series.py +447 -0
- quantml/training/__init__.py +135 -0
- quantml/training/alpha_eval.py +203 -0
- quantml/training/backtest.py +280 -0
- quantml/training/backtest_analysis.py +168 -0
- quantml/training/cv.py +106 -0
- quantml/training/data_loader.py +177 -0
- quantml/training/ensemble.py +84 -0
- quantml/training/feature_importance.py +135 -0
- quantml/training/features.py +364 -0
- quantml/training/futures_backtest.py +266 -0
- quantml/training/gradient_clipping.py +206 -0
- quantml/training/losses.py +248 -0
- quantml/training/lr_finder.py +127 -0
- quantml/training/metrics.py +376 -0
- quantml/training/regularization.py +89 -0
- quantml/training/trainer.py +239 -0
- quantml/training/walk_forward.py +190 -0
- quantml/utils/__init__.py +51 -0
- quantml/utils/gradient_check.py +274 -0
- quantml/utils/logging.py +181 -0
- quantml/utils/ops_cpu.py +231 -0
- quantml/utils/profiling.py +364 -0
- quantml/utils/reproducibility.py +220 -0
- quantml/utils/serialization.py +335 -0
- quantmllibrary-0.1.0.dist-info/METADATA +536 -0
- quantmllibrary-0.1.0.dist-info/RECORD +79 -0
- quantmllibrary-0.1.0.dist-info/WHEEL +5 -0
- quantmllibrary-0.1.0.dist-info/licenses/LICENSE +22 -0
- quantmllibrary-0.1.0.dist-info/top_level.txt +1 -0
quantml/features/base.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base feature class for plugin-based feature system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List, Dict, Any, Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class FeatureMetadata:
|
|
12
|
+
"""Metadata for a feature."""
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
formula: Optional[str] = None
|
|
16
|
+
expected_range: Optional[tuple] = None
|
|
17
|
+
unit: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseFeature(ABC):
|
|
21
|
+
"""
|
|
22
|
+
Base class for all features.
|
|
23
|
+
|
|
24
|
+
All features should inherit from this class and implement the compute method.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: str, description: str = "", **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
Initialize feature.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Feature name
|
|
33
|
+
description: Feature description
|
|
34
|
+
**kwargs: Feature-specific parameters
|
|
35
|
+
"""
|
|
36
|
+
self.name = name
|
|
37
|
+
self.description = description
|
|
38
|
+
self.params = kwargs
|
|
39
|
+
self.metadata = FeatureMetadata(
|
|
40
|
+
name=name,
|
|
41
|
+
description=description
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
46
|
+
"""
|
|
47
|
+
Compute feature values.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
data: Dictionary with required data (e.g., {'price': [...], 'volume': [...]})
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of feature values
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def get_metadata(self) -> FeatureMetadata:
|
|
58
|
+
"""Get feature metadata."""
|
|
59
|
+
return self.metadata
|
|
60
|
+
|
|
61
|
+
def validate_data(self, data: Dict[str, List[float]], required_keys: List[str]) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Validate that required data keys are present.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
data: Data dictionary
|
|
67
|
+
required_keys: List of required keys
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if all required keys present
|
|
71
|
+
"""
|
|
72
|
+
return all(key in data for key in required_keys)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Feature(BaseFeature):
|
|
76
|
+
"""
|
|
77
|
+
Simple feature implementation.
|
|
78
|
+
|
|
79
|
+
Use this for simple features that don't need complex logic.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
name: str,
|
|
85
|
+
compute_fn: callable,
|
|
86
|
+
description: str = "",
|
|
87
|
+
**kwargs
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Initialize feature with compute function.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Feature name
|
|
94
|
+
compute_fn: Function to compute feature: compute_fn(data) -> List[float]
|
|
95
|
+
description: Feature description
|
|
96
|
+
**kwargs: Additional parameters
|
|
97
|
+
"""
|
|
98
|
+
super().__init__(name, description, **kwargs)
|
|
99
|
+
self.compute_fn = compute_fn
|
|
100
|
+
|
|
101
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
102
|
+
"""Compute feature using provided function."""
|
|
103
|
+
return self.compute_fn(data, **self.params)
|
|
104
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overnight gap features for futures trading.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
from quantml.features.base import BaseFeature, FeatureMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OvernightGapFeature(BaseFeature):
|
|
10
|
+
"""
|
|
11
|
+
Overnight gap feature.
|
|
12
|
+
|
|
13
|
+
Computes the gap between previous day's close and current day's open.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, normalize: bool = True):
|
|
17
|
+
"""
|
|
18
|
+
Initialize overnight gap feature.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
normalize: Whether to normalize gap by previous close
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(
|
|
24
|
+
name="overnight_gap",
|
|
25
|
+
description="Gap between previous close and current open",
|
|
26
|
+
normalize=normalize
|
|
27
|
+
)
|
|
28
|
+
self.metadata.formula = "gap = (open(t) - close(t-1)) / close(t-1) if normalize else open(t) - close(t-1)"
|
|
29
|
+
self.metadata.expected_range = (-0.1, 0.1) if normalize else None
|
|
30
|
+
self.metadata.unit = "fraction" if normalize else "price_units"
|
|
31
|
+
|
|
32
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
33
|
+
"""Compute overnight gaps."""
|
|
34
|
+
if not self.validate_data(data, ['open', 'close']):
|
|
35
|
+
raise ValueError("OvernightGapFeature requires 'open' and 'close' in data")
|
|
36
|
+
|
|
37
|
+
opens = data['open']
|
|
38
|
+
closes = data['close']
|
|
39
|
+
|
|
40
|
+
gaps = [0.0] # First gap is 0 (no previous close)
|
|
41
|
+
|
|
42
|
+
for i in range(1, len(opens)):
|
|
43
|
+
if closes[i-1] > 0:
|
|
44
|
+
if self.params.get('normalize', True):
|
|
45
|
+
gap = (opens[i] - closes[i-1]) / closes[i-1]
|
|
46
|
+
else:
|
|
47
|
+
gap = opens[i] - closes[i-1]
|
|
48
|
+
else:
|
|
49
|
+
gap = 0.0
|
|
50
|
+
gaps.append(gap)
|
|
51
|
+
|
|
52
|
+
return gaps
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GapSizeFeature(BaseFeature):
|
|
56
|
+
"""
|
|
57
|
+
Gap size feature (absolute value of gap).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, normalize: bool = True):
|
|
61
|
+
"""
|
|
62
|
+
Initialize gap size feature.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
normalize: Whether to normalize by previous close
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(
|
|
68
|
+
name="gap_size",
|
|
69
|
+
description="Absolute size of overnight gap",
|
|
70
|
+
normalize=normalize
|
|
71
|
+
)
|
|
72
|
+
self.metadata.formula = "gap_size = |gap|"
|
|
73
|
+
self.metadata.expected_range = (0.0, 0.1) if normalize else None
|
|
74
|
+
|
|
75
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]):
|
|
76
|
+
"""Compute gap sizes."""
|
|
77
|
+
gap_feature = OvernightGapFeature(normalize=self.params.get('normalize', True))
|
|
78
|
+
gaps = gap_feature.compute(data)
|
|
79
|
+
return [abs(g) for g in gaps]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GapClosureFeature(BaseFeature):
|
|
83
|
+
"""
|
|
84
|
+
Gap closure feature (whether gap closed during the day).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
"""Initialize gap closure feature."""
|
|
89
|
+
super().__init__(
|
|
90
|
+
name="gap_closure",
|
|
91
|
+
description="Binary indicator if overnight gap closed during day"
|
|
92
|
+
)
|
|
93
|
+
self.metadata.formula = "gap_closed = 1 if gap closed, 0 otherwise"
|
|
94
|
+
self.metadata.expected_range = (0, 1)
|
|
95
|
+
|
|
96
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
97
|
+
"""Compute gap closure indicators."""
|
|
98
|
+
if not self.validate_data(data, ['open', 'close', 'high', 'low']):
|
|
99
|
+
raise ValueError("GapClosureFeature requires 'open', 'close', 'high', 'low'")
|
|
100
|
+
|
|
101
|
+
opens = data['open']
|
|
102
|
+
closes = data['close']
|
|
103
|
+
highs = data['high']
|
|
104
|
+
lows = data['low']
|
|
105
|
+
|
|
106
|
+
closures = [0.0] # First day has no gap
|
|
107
|
+
|
|
108
|
+
for i in range(1, len(opens)):
|
|
109
|
+
prev_close = closes[i-1]
|
|
110
|
+
gap = opens[i] - prev_close
|
|
111
|
+
|
|
112
|
+
if abs(gap) < 0.0001: # No gap
|
|
113
|
+
closures.append(0.0)
|
|
114
|
+
elif gap > 0: # Gap up
|
|
115
|
+
# Gap closed if low <= prev_close
|
|
116
|
+
closed = 1.0 if lows[i] <= prev_close else 0.0
|
|
117
|
+
closures.append(closed)
|
|
118
|
+
else: # Gap down
|
|
119
|
+
# Gap closed if high >= prev_close
|
|
120
|
+
closed = 1.0 if highs[i] >= prev_close else 0.0
|
|
121
|
+
closures.append(closed)
|
|
122
|
+
|
|
123
|
+
return closures
|
|
124
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feature registry for plugin-based feature system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Callable
|
|
6
|
+
from quantml.features.base import BaseFeature, FeatureMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FeatureRegistry:
|
|
10
|
+
"""Registry for managing features."""
|
|
11
|
+
|
|
12
|
+
_instance = None
|
|
13
|
+
_features: Dict[str, BaseFeature] = {}
|
|
14
|
+
|
|
15
|
+
def __new__(cls):
|
|
16
|
+
if cls._instance is None:
|
|
17
|
+
cls._instance = super().__new__(cls)
|
|
18
|
+
return cls._instance
|
|
19
|
+
|
|
20
|
+
def register(self, feature: BaseFeature, name: Optional[str] = None):
|
|
21
|
+
"""
|
|
22
|
+
Register a feature.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
feature: Feature instance
|
|
26
|
+
name: Optional name override (default: feature.name)
|
|
27
|
+
"""
|
|
28
|
+
feature_name = name or feature.name
|
|
29
|
+
self._features[feature_name] = feature
|
|
30
|
+
|
|
31
|
+
def get(self, name: str) -> Optional[BaseFeature]:
|
|
32
|
+
"""
|
|
33
|
+
Get feature by name.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name: Feature name
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Feature instance or None
|
|
40
|
+
"""
|
|
41
|
+
return self._features.get(name)
|
|
42
|
+
|
|
43
|
+
def list_features(self) -> List[str]:
|
|
44
|
+
"""List all registered feature names."""
|
|
45
|
+
return list(self._features.keys())
|
|
46
|
+
|
|
47
|
+
def get_all(self) -> Dict[str, BaseFeature]:
|
|
48
|
+
"""Get all registered features."""
|
|
49
|
+
return self._features.copy()
|
|
50
|
+
|
|
51
|
+
def compute_features(
|
|
52
|
+
self,
|
|
53
|
+
feature_names: List[str],
|
|
54
|
+
data: Dict[str, List[float]]
|
|
55
|
+
) -> Dict[str, List[float]]:
|
|
56
|
+
"""
|
|
57
|
+
Compute multiple features.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
feature_names: List of feature names to compute
|
|
61
|
+
data: Input data dictionary
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dictionary of feature name -> feature values
|
|
65
|
+
"""
|
|
66
|
+
results = {}
|
|
67
|
+
|
|
68
|
+
for name in feature_names:
|
|
69
|
+
feature = self.get(name)
|
|
70
|
+
if feature is None:
|
|
71
|
+
raise ValueError(f"Feature not found: {name}")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
values = feature.compute(data)
|
|
75
|
+
results[name] = values
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise RuntimeError(f"Error computing feature {name}: {e}")
|
|
78
|
+
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
def get_metadata(self, name: str) -> Optional[FeatureMetadata]:
|
|
82
|
+
"""
|
|
83
|
+
Get feature metadata.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Feature name
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
FeatureMetadata or None
|
|
90
|
+
"""
|
|
91
|
+
feature = self.get(name)
|
|
92
|
+
return feature.get_metadata() if feature else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Global registry instance
|
|
96
|
+
_registry = FeatureRegistry()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_feature(feature: BaseFeature, name: Optional[str] = None):
|
|
100
|
+
"""
|
|
101
|
+
Register a feature in the global registry.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
feature: Feature instance
|
|
105
|
+
name: Optional name override
|
|
106
|
+
"""
|
|
107
|
+
_registry.register(feature, name)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_feature(name: str) -> Optional[BaseFeature]:
|
|
111
|
+
"""
|
|
112
|
+
Get feature from global registry.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
name: Feature name
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Feature instance or None
|
|
119
|
+
"""
|
|
120
|
+
return _registry.get(name)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def compute_features(
|
|
124
|
+
feature_names: List[str],
|
|
125
|
+
data: Dict[str, List[float]]
|
|
126
|
+
) -> Dict[str, List[float]]:
|
|
127
|
+
"""
|
|
128
|
+
Compute multiple features from registry.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
feature_names: List of feature names
|
|
132
|
+
data: Input data
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary of computed features
|
|
136
|
+
"""
|
|
137
|
+
return _registry.compute_features(feature_names, data)
|
|
138
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volatility-based features.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
from quantml.features.base import BaseFeature, FeatureMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VolatilityRegimeFeature(BaseFeature):
|
|
10
|
+
"""
|
|
11
|
+
Volatility regime feature (low/normal/high volatility).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, window: int = 20, low_threshold: float = 0.25, high_threshold: float = 0.75):
|
|
15
|
+
"""
|
|
16
|
+
Initialize volatility regime feature.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
window: Window for volatility calculation
|
|
20
|
+
low_threshold: Percentile for low volatility
|
|
21
|
+
high_threshold: Percentile for high volatility
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(
|
|
24
|
+
name="volatility_regime",
|
|
25
|
+
description="Volatility regime (0=low, 1=normal, 2=high)",
|
|
26
|
+
window=window,
|
|
27
|
+
low_threshold=low_threshold,
|
|
28
|
+
high_threshold=high_threshold
|
|
29
|
+
)
|
|
30
|
+
self.metadata.formula = "regime based on realized volatility percentiles"
|
|
31
|
+
self.metadata.expected_range = (0, 2)
|
|
32
|
+
|
|
33
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
34
|
+
"""Compute volatility regimes."""
|
|
35
|
+
if not self.validate_data(data, ['price']):
|
|
36
|
+
raise ValueError("VolatilityRegimeFeature requires 'price' in data")
|
|
37
|
+
|
|
38
|
+
prices = data['price']
|
|
39
|
+
window = self.params.get('window', 20)
|
|
40
|
+
|
|
41
|
+
# Calculate returns
|
|
42
|
+
returns = []
|
|
43
|
+
for i in range(1, len(prices)):
|
|
44
|
+
if prices[i-1] > 0:
|
|
45
|
+
ret = (prices[i] - prices[i-1]) / prices[i-1]
|
|
46
|
+
else:
|
|
47
|
+
ret = 0.0
|
|
48
|
+
returns.append(ret)
|
|
49
|
+
|
|
50
|
+
# Calculate rolling volatility
|
|
51
|
+
volatilities = []
|
|
52
|
+
for i in range(window, len(returns)):
|
|
53
|
+
window_rets = returns[i-window:i]
|
|
54
|
+
mean_ret = sum(window_rets) / len(window_rets)
|
|
55
|
+
variance = sum((r - mean_ret) ** 2 for r in window_rets) / len(window_rets)
|
|
56
|
+
vol = variance ** 0.5
|
|
57
|
+
volatilities.append(vol)
|
|
58
|
+
|
|
59
|
+
if len(volatilities) < 2:
|
|
60
|
+
return [1.0] * len(prices) # Default to normal
|
|
61
|
+
|
|
62
|
+
# Calculate percentiles
|
|
63
|
+
sorted_vols = sorted(volatilities)
|
|
64
|
+
low_idx = int(len(sorted_vols) * self.params['low_threshold'])
|
|
65
|
+
high_idx = int(len(sorted_vols) * self.params['high_threshold'])
|
|
66
|
+
|
|
67
|
+
low_threshold = sorted_vols[low_idx]
|
|
68
|
+
high_threshold = sorted_vols[high_idx]
|
|
69
|
+
|
|
70
|
+
# Map to regimes
|
|
71
|
+
regimes = [1.0] * window # Not enough data
|
|
72
|
+
|
|
73
|
+
for vol in volatilities:
|
|
74
|
+
if vol < low_threshold:
|
|
75
|
+
regimes.append(0.0)
|
|
76
|
+
elif vol >= high_threshold:
|
|
77
|
+
regimes.append(2.0)
|
|
78
|
+
else:
|
|
79
|
+
regimes.append(1.0)
|
|
80
|
+
|
|
81
|
+
return regimes
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RealizedVolatilityFeature(BaseFeature):
|
|
85
|
+
"""
|
|
86
|
+
Realized volatility feature.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, window: int = 20, annualize: bool = True):
|
|
90
|
+
"""
|
|
91
|
+
Initialize realized volatility feature.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
window: Rolling window
|
|
95
|
+
annualize: Whether to annualize (multiply by sqrt(252))
|
|
96
|
+
"""
|
|
97
|
+
super().__init__(
|
|
98
|
+
name="realized_volatility",
|
|
99
|
+
description="Realized volatility (std of returns)",
|
|
100
|
+
window=window,
|
|
101
|
+
annualize=annualize
|
|
102
|
+
)
|
|
103
|
+
self.metadata.formula = "vol = std(returns) * sqrt(252) if annualize"
|
|
104
|
+
self.metadata.expected_range = (0.0, 1.0)
|
|
105
|
+
|
|
106
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
107
|
+
"""Compute realized volatility."""
|
|
108
|
+
if not self.validate_data(data, ['price']):
|
|
109
|
+
raise ValueError("RealizedVolatilityFeature requires 'price' in data")
|
|
110
|
+
|
|
111
|
+
prices = data['price']
|
|
112
|
+
window = self.params.get('window', 20)
|
|
113
|
+
annualize = self.params.get('annualize', True)
|
|
114
|
+
|
|
115
|
+
# Calculate returns
|
|
116
|
+
returns = []
|
|
117
|
+
for i in range(1, len(prices)):
|
|
118
|
+
if prices[i-1] > 0:
|
|
119
|
+
ret = (prices[i] - prices[i-1]) / prices[i-1]
|
|
120
|
+
else:
|
|
121
|
+
ret = 0.0
|
|
122
|
+
returns.append(ret)
|
|
123
|
+
|
|
124
|
+
# Calculate rolling volatility
|
|
125
|
+
volatilities = [0.0] * min(window, len(prices))
|
|
126
|
+
|
|
127
|
+
for i in range(window, len(returns)):
|
|
128
|
+
window_rets = returns[i-window:i]
|
|
129
|
+
mean_ret = sum(window_rets) / len(window_rets)
|
|
130
|
+
variance = sum((r - mean_ret) ** 2 for r in window_rets) / len(window_rets)
|
|
131
|
+
vol = variance ** 0.5
|
|
132
|
+
|
|
133
|
+
if annualize:
|
|
134
|
+
import math
|
|
135
|
+
vol = vol * math.sqrt(252)
|
|
136
|
+
|
|
137
|
+
volatilities.append(vol)
|
|
138
|
+
|
|
139
|
+
return volatilities
|
|
140
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volume-based features.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
from quantml.features.base import BaseFeature, FeatureMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VolumeRegimeFeature(BaseFeature):
|
|
10
|
+
"""
|
|
11
|
+
Volume regime feature (low/normal/high volume).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, low_threshold: float = 0.25, high_threshold: float = 0.75):
|
|
15
|
+
"""
|
|
16
|
+
Initialize volume regime feature.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
low_threshold: Percentile for low volume (default: 25th)
|
|
20
|
+
high_threshold: Percentile for high volume (default: 75th)
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(
|
|
23
|
+
name="volume_regime",
|
|
24
|
+
description="Volume regime classification (0=low, 1=normal, 2=high)",
|
|
25
|
+
low_threshold=low_threshold,
|
|
26
|
+
high_threshold=high_threshold
|
|
27
|
+
)
|
|
28
|
+
self.metadata.formula = "regime = 0 if volume < p25, 1 if p25 <= volume < p75, 2 if volume >= p75"
|
|
29
|
+
self.metadata.expected_range = (0, 2)
|
|
30
|
+
|
|
31
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
32
|
+
"""Compute volume regimes."""
|
|
33
|
+
if not self.validate_data(data, ['volume']):
|
|
34
|
+
raise ValueError("VolumeRegimeFeature requires 'volume' in data")
|
|
35
|
+
|
|
36
|
+
volumes = data['volume']
|
|
37
|
+
|
|
38
|
+
if len(volumes) < 20:
|
|
39
|
+
return [1.0] * len(volumes) # Default to normal
|
|
40
|
+
|
|
41
|
+
# Calculate percentiles
|
|
42
|
+
sorted_vols = sorted(volumes)
|
|
43
|
+
low_idx = int(len(sorted_vols) * self.params['low_threshold'])
|
|
44
|
+
high_idx = int(len(sorted_vols) * self.params['high_threshold'])
|
|
45
|
+
|
|
46
|
+
low_threshold = sorted_vols[low_idx]
|
|
47
|
+
high_threshold = sorted_vols[high_idx]
|
|
48
|
+
|
|
49
|
+
regimes = []
|
|
50
|
+
for vol in volumes:
|
|
51
|
+
if vol < low_threshold:
|
|
52
|
+
regimes.append(0.0)
|
|
53
|
+
elif vol >= high_threshold:
|
|
54
|
+
regimes.append(2.0)
|
|
55
|
+
else:
|
|
56
|
+
regimes.append(1.0)
|
|
57
|
+
|
|
58
|
+
return regimes
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VolumeShockFeature(BaseFeature):
|
|
62
|
+
"""
|
|
63
|
+
Volume shock feature (unusual volume).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, window: int = 20, threshold: float = 2.0):
|
|
67
|
+
"""
|
|
68
|
+
Initialize volume shock feature.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
window: Rolling window for volume average
|
|
72
|
+
threshold: Multiplier threshold for shock (default: 2x average)
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(
|
|
75
|
+
name="volume_shock",
|
|
76
|
+
description="Binary indicator for volume shock (1 if volume > threshold * avg)",
|
|
77
|
+
window=window,
|
|
78
|
+
threshold=threshold
|
|
79
|
+
)
|
|
80
|
+
self.metadata.formula = "shock = 1 if volume > threshold * rolling_mean(volume, window)"
|
|
81
|
+
self.metadata.expected_range = (0, 1)
|
|
82
|
+
|
|
83
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
84
|
+
"""Compute volume shocks."""
|
|
85
|
+
if not self.validate_data(data, ['volume']):
|
|
86
|
+
raise ValueError("VolumeShockFeature requires 'volume' in data")
|
|
87
|
+
|
|
88
|
+
volumes = data['volume']
|
|
89
|
+
window = self.params.get('window', 20)
|
|
90
|
+
threshold = self.params.get('threshold', 2.0)
|
|
91
|
+
|
|
92
|
+
shocks = [0.0] * min(window, len(volumes)) # Not enough data
|
|
93
|
+
|
|
94
|
+
for i in range(window, len(volumes)):
|
|
95
|
+
window_vols = volumes[i-window:i]
|
|
96
|
+
avg_volume = sum(window_vols) / len(window_vols)
|
|
97
|
+
|
|
98
|
+
shock = 1.0 if volumes[i] > threshold * avg_volume else 0.0
|
|
99
|
+
shocks.append(shock)
|
|
100
|
+
|
|
101
|
+
return shocks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class VolumeRatioFeature(BaseFeature):
|
|
105
|
+
"""
|
|
106
|
+
Volume ratio feature (current volume / average volume).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, window: int = 20):
|
|
110
|
+
"""
|
|
111
|
+
Initialize volume ratio feature.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
window: Rolling window for average volume
|
|
115
|
+
"""
|
|
116
|
+
super().__init__(
|
|
117
|
+
name="volume_ratio",
|
|
118
|
+
description="Ratio of current volume to rolling average",
|
|
119
|
+
window=window
|
|
120
|
+
)
|
|
121
|
+
self.metadata.formula = "volume_ratio = volume(t) / mean(volume(t-window:t))"
|
|
122
|
+
self.metadata.expected_range = (0.0, 10.0)
|
|
123
|
+
|
|
124
|
+
def compute(self, data: Dict[str, List[float]]) -> List[float]:
|
|
125
|
+
"""Compute volume ratios."""
|
|
126
|
+
if not self.validate_data(data, ['volume']):
|
|
127
|
+
raise ValueError("VolumeRatioFeature requires 'volume' in data")
|
|
128
|
+
|
|
129
|
+
volumes = data['volume']
|
|
130
|
+
window = self.params.get('window', 20)
|
|
131
|
+
|
|
132
|
+
ratios = [1.0] * min(window, len(volumes))
|
|
133
|
+
|
|
134
|
+
for i in range(window, len(volumes)):
|
|
135
|
+
window_vols = volumes[i-window:i]
|
|
136
|
+
avg_volume = sum(window_vols) / len(window_vols)
|
|
137
|
+
|
|
138
|
+
ratio = volumes[i] / avg_volume if avg_volume > 0 else 1.0
|
|
139
|
+
ratios.append(ratio)
|
|
140
|
+
|
|
141
|
+
return ratios
|
|
142
|
+
|
quantml/functional.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functional API for QuantML operations.
|
|
3
|
+
|
|
4
|
+
This module provides a convenient functional interface to all operations,
|
|
5
|
+
similar to PyTorch's functional API. Use this for a cleaner API when
|
|
6
|
+
composing operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Union, Optional
|
|
10
|
+
from quantml.tensor import Tensor
|
|
11
|
+
from quantml import ops
|
|
12
|
+
|
|
13
|
+
# Re-export all operations with F. prefix
|
|
14
|
+
add = ops.add
|
|
15
|
+
sub = ops.sub
|
|
16
|
+
mul = ops.mul
|
|
17
|
+
div = ops.div
|
|
18
|
+
pow = ops.pow
|
|
19
|
+
matmul = ops.matmul
|
|
20
|
+
dot = ops.dot
|
|
21
|
+
sum = ops.sum
|
|
22
|
+
mean = ops.mean
|
|
23
|
+
std = ops.std
|
|
24
|
+
relu = ops.relu
|
|
25
|
+
sigmoid = ops.sigmoid
|
|
26
|
+
tanh = ops.tanh
|
|
27
|
+
abs = ops.abs
|
|
28
|
+
maximum = ops.maximum
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'add', 'sub', 'mul', 'div', 'pow',
|
|
32
|
+
'matmul', 'dot',
|
|
33
|
+
'sum', 'mean', 'std',
|
|
34
|
+
'relu', 'sigmoid', 'tanh',
|
|
35
|
+
'abs', 'maximum'
|
|
36
|
+
]
|
|
37
|
+
|