quantumflow-sdk 0.2.1__py3-none-any.whl → 0.4.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.
- api/main.py +34 -3
- api/models.py +41 -0
- api/routes/algorithm_routes.py +1029 -0
- api/routes/chat_routes.py +565 -0
- api/routes/pipeline_routes.py +578 -0
- db/models.py +357 -0
- quantumflow/algorithms/machine_learning/__init__.py +14 -2
- quantumflow/algorithms/machine_learning/vqe.py +355 -3
- quantumflow/core/__init__.py +10 -1
- quantumflow/core/quantum_compressor.py +379 -1
- quantumflow/integrations/domain_agents.py +617 -0
- quantumflow/pipeline/__init__.py +29 -0
- quantumflow/pipeline/anomaly_detector.py +521 -0
- quantumflow/pipeline/base_pipeline.py +602 -0
- quantumflow/pipeline/checkpoint_manager.py +587 -0
- quantumflow/pipeline/finance/__init__.py +5 -0
- quantumflow/pipeline/finance/portfolio_optimization.py +595 -0
- quantumflow/pipeline/healthcare/__init__.py +5 -0
- quantumflow/pipeline/healthcare/protein_folding.py +994 -0
- quantumflow/pipeline/temporal_memory.py +577 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/METADATA +3 -3
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/RECORD +25 -12
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
- {quantumflow_sdk-0.2.1.dist-info → quantumflow_sdk-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anomaly Detection for Data Pipelines.
|
|
3
|
+
|
|
4
|
+
Detects:
|
|
5
|
+
- Gradient explosion (>threshold) and vanishing (<threshold)
|
|
6
|
+
- NaN/Inf values
|
|
7
|
+
- Statistical deviations (moving average, z-score)
|
|
8
|
+
- Domain-specific anomalies (energy spikes, RMSD divergence, VaR breach)
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
detector = AnomalyDetector()
|
|
12
|
+
|
|
13
|
+
# Add custom detector
|
|
14
|
+
detector.register_detector("my_detector", my_detector_fn)
|
|
15
|
+
|
|
16
|
+
# Detect anomalies
|
|
17
|
+
result = detector.detect(state, step=100)
|
|
18
|
+
if result.is_anomaly:
|
|
19
|
+
print(f"Anomaly: {result.anomaly_type}")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
import logging
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AnomalyType(str, Enum):
|
|
32
|
+
"""Types of anomalies that can be detected."""
|
|
33
|
+
|
|
34
|
+
GRADIENT_EXPLOSION = "gradient_explosion"
|
|
35
|
+
GRADIENT_VANISHING = "gradient_vanishing"
|
|
36
|
+
NAN_DETECTED = "nan_detected"
|
|
37
|
+
INF_DETECTED = "inf_detected"
|
|
38
|
+
ENERGY_SPIKE = "energy_spike"
|
|
39
|
+
RMSD_DIVERGENCE = "rmsd_divergence"
|
|
40
|
+
VAR_BREACH = "var_breach"
|
|
41
|
+
DRAWDOWN_BREACH = "drawdown_breach"
|
|
42
|
+
STATISTICAL_DEVIATION = "statistical_deviation"
|
|
43
|
+
CUSTOM = "custom"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AnomalySeverity(str, Enum):
|
|
47
|
+
"""Severity levels for anomalies."""
|
|
48
|
+
|
|
49
|
+
INFO = "info"
|
|
50
|
+
WARNING = "warning"
|
|
51
|
+
CRITICAL = "critical"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class AnomalyResult:
|
|
56
|
+
"""Result of anomaly detection."""
|
|
57
|
+
|
|
58
|
+
is_anomaly: bool
|
|
59
|
+
anomaly_type: Optional[str] = None
|
|
60
|
+
severity: str = "warning"
|
|
61
|
+
message: str = ""
|
|
62
|
+
step: int = 0
|
|
63
|
+
detector_name: Optional[str] = None
|
|
64
|
+
threshold: Optional[float] = None
|
|
65
|
+
actual_value: Optional[float] = None
|
|
66
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
"""Convert to dictionary."""
|
|
70
|
+
return {
|
|
71
|
+
"is_anomaly": self.is_anomaly,
|
|
72
|
+
"anomaly_type": self.anomaly_type,
|
|
73
|
+
"severity": self.severity,
|
|
74
|
+
"message": self.message,
|
|
75
|
+
"step": self.step,
|
|
76
|
+
"detector_name": self.detector_name,
|
|
77
|
+
"threshold": self.threshold,
|
|
78
|
+
"actual_value": self.actual_value,
|
|
79
|
+
"data": self.data,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Type for custom detector functions
|
|
84
|
+
DetectorFn = Callable[["PipelineState", int, Dict[str, Any]], Optional[AnomalyResult]]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AnomalyDetector:
|
|
88
|
+
"""
|
|
89
|
+
Detects anomalies in pipeline execution.
|
|
90
|
+
|
|
91
|
+
Built-in detectors:
|
|
92
|
+
- Gradient explosion/vanishing
|
|
93
|
+
- NaN/Inf detection
|
|
94
|
+
- Statistical deviation (moving average, z-score)
|
|
95
|
+
|
|
96
|
+
Supports custom detector registration for domain-specific checks.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
window_size: int = 50,
|
|
102
|
+
z_score_threshold: float = 3.0,
|
|
103
|
+
moving_avg_deviation: float = 2.0,
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Initialize anomaly detector.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
window_size: Window for moving statistics
|
|
110
|
+
z_score_threshold: Z-score threshold for statistical anomalies
|
|
111
|
+
moving_avg_deviation: Multiplier for moving average deviation
|
|
112
|
+
"""
|
|
113
|
+
self.window_size = window_size
|
|
114
|
+
self.z_score_threshold = z_score_threshold
|
|
115
|
+
self.moving_avg_deviation = moving_avg_deviation
|
|
116
|
+
|
|
117
|
+
# Custom detectors
|
|
118
|
+
self._custom_detectors: Dict[str, DetectorFn] = {}
|
|
119
|
+
|
|
120
|
+
# History for statistical analysis
|
|
121
|
+
self._metric_history: Dict[str, List[float]] = {}
|
|
122
|
+
|
|
123
|
+
def register_detector(self, name: str, detector_fn: DetectorFn):
|
|
124
|
+
"""
|
|
125
|
+
Register a custom anomaly detector.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
name: Detector name
|
|
129
|
+
detector_fn: Function(state, step, config) -> Optional[AnomalyResult]
|
|
130
|
+
"""
|
|
131
|
+
self._custom_detectors[name] = detector_fn
|
|
132
|
+
logger.info(f"Registered custom detector: {name}")
|
|
133
|
+
|
|
134
|
+
def unregister_detector(self, name: str):
|
|
135
|
+
"""Unregister a custom detector."""
|
|
136
|
+
if name in self._custom_detectors:
|
|
137
|
+
del self._custom_detectors[name]
|
|
138
|
+
|
|
139
|
+
def detect(
|
|
140
|
+
self,
|
|
141
|
+
state: "PipelineState",
|
|
142
|
+
step: int,
|
|
143
|
+
gradient_threshold: float = 100.0,
|
|
144
|
+
vanishing_threshold: float = 1e-7,
|
|
145
|
+
config: Optional[Dict[str, Any]] = None,
|
|
146
|
+
) -> Optional[AnomalyResult]:
|
|
147
|
+
"""
|
|
148
|
+
Run all anomaly detectors on current state.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
state: Current pipeline state
|
|
152
|
+
step: Current step number
|
|
153
|
+
gradient_threshold: Threshold for gradient explosion
|
|
154
|
+
vanishing_threshold: Threshold for vanishing gradients
|
|
155
|
+
config: Additional configuration for detectors
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
AnomalyResult if anomaly detected, None otherwise
|
|
159
|
+
"""
|
|
160
|
+
config = config or {}
|
|
161
|
+
|
|
162
|
+
# Check NaN/Inf first (most critical)
|
|
163
|
+
result = self._check_nan_inf(state, step)
|
|
164
|
+
if result:
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
# Check gradients
|
|
168
|
+
result = self._check_gradients(
|
|
169
|
+
state, step, gradient_threshold, vanishing_threshold
|
|
170
|
+
)
|
|
171
|
+
if result:
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
# Check statistical deviation
|
|
175
|
+
result = self._check_statistical(state, step)
|
|
176
|
+
if result:
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
# Run custom detectors
|
|
180
|
+
for name, detector_fn in self._custom_detectors.items():
|
|
181
|
+
try:
|
|
182
|
+
result = detector_fn(state, step, config)
|
|
183
|
+
if result and result.is_anomaly:
|
|
184
|
+
result.detector_name = name
|
|
185
|
+
return result
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Custom detector {name} failed: {e}")
|
|
188
|
+
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def _check_nan_inf(self, state: "PipelineState", step: int) -> Optional[AnomalyResult]:
|
|
192
|
+
"""Check for NaN or Inf values in state."""
|
|
193
|
+
values_to_check = []
|
|
194
|
+
|
|
195
|
+
# Check metrics
|
|
196
|
+
for name, value in state.metrics.items():
|
|
197
|
+
if isinstance(value, (int, float)):
|
|
198
|
+
values_to_check.append((f"metrics.{name}", value))
|
|
199
|
+
|
|
200
|
+
# Check gradients
|
|
201
|
+
for i, grad in enumerate(state.gradient_history[-10:]):
|
|
202
|
+
values_to_check.append((f"gradient[{i}]", grad))
|
|
203
|
+
|
|
204
|
+
# Check weights
|
|
205
|
+
if state.weights:
|
|
206
|
+
for i, w in enumerate(state.weights[:100]): # Check first 100
|
|
207
|
+
values_to_check.append((f"weight[{i}]", w))
|
|
208
|
+
|
|
209
|
+
# Detect NaN
|
|
210
|
+
for name, value in values_to_check:
|
|
211
|
+
if isinstance(value, float) and math.isnan(value):
|
|
212
|
+
return AnomalyResult(
|
|
213
|
+
is_anomaly=True,
|
|
214
|
+
anomaly_type=AnomalyType.NAN_DETECTED.value,
|
|
215
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
216
|
+
message=f"NaN detected in {name}",
|
|
217
|
+
step=step,
|
|
218
|
+
detector_name="nan_inf_detector",
|
|
219
|
+
actual_value=value,
|
|
220
|
+
data={"location": name},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Detect Inf
|
|
224
|
+
for name, value in values_to_check:
|
|
225
|
+
if isinstance(value, float) and math.isinf(value):
|
|
226
|
+
return AnomalyResult(
|
|
227
|
+
is_anomaly=True,
|
|
228
|
+
anomaly_type=AnomalyType.INF_DETECTED.value,
|
|
229
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
230
|
+
message=f"Inf detected in {name}",
|
|
231
|
+
step=step,
|
|
232
|
+
detector_name="nan_inf_detector",
|
|
233
|
+
actual_value=value,
|
|
234
|
+
data={"location": name},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def _check_gradients(
|
|
240
|
+
self,
|
|
241
|
+
state: "PipelineState",
|
|
242
|
+
step: int,
|
|
243
|
+
explosion_threshold: float,
|
|
244
|
+
vanishing_threshold: float,
|
|
245
|
+
) -> Optional[AnomalyResult]:
|
|
246
|
+
"""Check for gradient explosion or vanishing."""
|
|
247
|
+
if not state.gradient_history:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
# Get recent gradient magnitude
|
|
251
|
+
recent_grads = state.gradient_history[-10:]
|
|
252
|
+
if not recent_grads:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
# Compute gradient norm (average magnitude)
|
|
256
|
+
grad_norm = sum(abs(g) for g in recent_grads) / len(recent_grads)
|
|
257
|
+
|
|
258
|
+
# Check explosion
|
|
259
|
+
if grad_norm > explosion_threshold:
|
|
260
|
+
return AnomalyResult(
|
|
261
|
+
is_anomaly=True,
|
|
262
|
+
anomaly_type=AnomalyType.GRADIENT_EXPLOSION.value,
|
|
263
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
264
|
+
message=f"Gradient explosion detected: {grad_norm:.2e} > {explosion_threshold}",
|
|
265
|
+
step=step,
|
|
266
|
+
detector_name="gradient_detector",
|
|
267
|
+
threshold=explosion_threshold,
|
|
268
|
+
actual_value=grad_norm,
|
|
269
|
+
data={"recent_gradients": recent_grads},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Check vanishing
|
|
273
|
+
if grad_norm < vanishing_threshold and grad_norm > 0:
|
|
274
|
+
return AnomalyResult(
|
|
275
|
+
is_anomaly=True,
|
|
276
|
+
anomaly_type=AnomalyType.GRADIENT_VANISHING.value,
|
|
277
|
+
severity=AnomalySeverity.WARNING.value,
|
|
278
|
+
message=f"Vanishing gradient detected: {grad_norm:.2e} < {vanishing_threshold}",
|
|
279
|
+
step=step,
|
|
280
|
+
detector_name="gradient_detector",
|
|
281
|
+
threshold=vanishing_threshold,
|
|
282
|
+
actual_value=grad_norm,
|
|
283
|
+
data={"recent_gradients": recent_grads},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def _check_statistical(
|
|
289
|
+
self, state: "PipelineState", step: int
|
|
290
|
+
) -> Optional[AnomalyResult]:
|
|
291
|
+
"""Check for statistical deviations in metrics."""
|
|
292
|
+
for metric_name, value in state.metrics.items():
|
|
293
|
+
if not isinstance(value, (int, float)):
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Update history
|
|
297
|
+
if metric_name not in self._metric_history:
|
|
298
|
+
self._metric_history[metric_name] = []
|
|
299
|
+
|
|
300
|
+
history = self._metric_history[metric_name]
|
|
301
|
+
history.append(value)
|
|
302
|
+
|
|
303
|
+
# Keep window size
|
|
304
|
+
if len(history) > self.window_size:
|
|
305
|
+
history.pop(0)
|
|
306
|
+
|
|
307
|
+
# Need enough history for statistics
|
|
308
|
+
if len(history) < 10:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Compute statistics
|
|
312
|
+
mean = sum(history) / len(history)
|
|
313
|
+
variance = sum((x - mean) ** 2 for x in history) / len(history)
|
|
314
|
+
std = math.sqrt(variance) if variance > 0 else 0
|
|
315
|
+
|
|
316
|
+
if std == 0:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Z-score check
|
|
320
|
+
z_score = abs(value - mean) / std
|
|
321
|
+
|
|
322
|
+
if z_score > self.z_score_threshold:
|
|
323
|
+
return AnomalyResult(
|
|
324
|
+
is_anomaly=True,
|
|
325
|
+
anomaly_type=AnomalyType.STATISTICAL_DEVIATION.value,
|
|
326
|
+
severity=AnomalySeverity.WARNING.value,
|
|
327
|
+
message=f"Statistical anomaly in {metric_name}: z-score={z_score:.2f}",
|
|
328
|
+
step=step,
|
|
329
|
+
detector_name="statistical_detector",
|
|
330
|
+
threshold=self.z_score_threshold,
|
|
331
|
+
actual_value=z_score,
|
|
332
|
+
data={
|
|
333
|
+
"metric_name": metric_name,
|
|
334
|
+
"metric_value": value,
|
|
335
|
+
"mean": mean,
|
|
336
|
+
"std": std,
|
|
337
|
+
"z_score": z_score,
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def clear_history(self):
|
|
344
|
+
"""Clear metric history."""
|
|
345
|
+
self._metric_history.clear()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ============================================================
|
|
349
|
+
# Domain-Specific Detector Factories
|
|
350
|
+
# ============================================================
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def create_energy_spike_detector(
|
|
354
|
+
threshold_multiplier: float = 5.0,
|
|
355
|
+
) -> DetectorFn:
|
|
356
|
+
"""
|
|
357
|
+
Create detector for energy spikes (healthcare/chemistry).
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
threshold_multiplier: Multiplier for energy spike detection
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Detector function
|
|
364
|
+
"""
|
|
365
|
+
energy_history: List[float] = []
|
|
366
|
+
|
|
367
|
+
def detector(state: "PipelineState", step: int, config: Dict[str, Any]) -> Optional[AnomalyResult]:
|
|
368
|
+
energy = state.metrics.get("energy") or state.metrics.get("ground_energy")
|
|
369
|
+
|
|
370
|
+
if energy is None:
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
energy_history.append(energy)
|
|
374
|
+
if len(energy_history) > 100:
|
|
375
|
+
energy_history.pop(0)
|
|
376
|
+
|
|
377
|
+
if len(energy_history) < 5:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
# Check for sudden spike
|
|
381
|
+
recent_mean = sum(energy_history[-10:-1]) / (len(energy_history[-10:-1]) or 1)
|
|
382
|
+
if recent_mean == 0:
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
spike_ratio = abs(energy - recent_mean) / abs(recent_mean)
|
|
386
|
+
|
|
387
|
+
if spike_ratio > threshold_multiplier:
|
|
388
|
+
return AnomalyResult(
|
|
389
|
+
is_anomaly=True,
|
|
390
|
+
anomaly_type=AnomalyType.ENERGY_SPIKE.value,
|
|
391
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
392
|
+
message=f"Energy spike detected: {spike_ratio:.2f}x above recent average",
|
|
393
|
+
step=step,
|
|
394
|
+
threshold=threshold_multiplier,
|
|
395
|
+
actual_value=spike_ratio,
|
|
396
|
+
data={"energy": energy, "recent_mean": recent_mean},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
return detector
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def create_rmsd_detector(max_rmsd: float = 10.0) -> DetectorFn:
|
|
405
|
+
"""
|
|
406
|
+
Create detector for RMSD divergence (protein folding).
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
max_rmsd: Maximum allowed RMSD in Angstroms
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Detector function
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
def detector(state: "PipelineState", step: int, config: Dict[str, Any]) -> Optional[AnomalyResult]:
|
|
416
|
+
rmsd = state.metrics.get("rmsd")
|
|
417
|
+
|
|
418
|
+
if rmsd is None:
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
if rmsd > max_rmsd:
|
|
422
|
+
return AnomalyResult(
|
|
423
|
+
is_anomaly=True,
|
|
424
|
+
anomaly_type=AnomalyType.RMSD_DIVERGENCE.value,
|
|
425
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
426
|
+
message=f"RMSD divergence: {rmsd:.2f}Å > {max_rmsd}Å threshold",
|
|
427
|
+
step=step,
|
|
428
|
+
threshold=max_rmsd,
|
|
429
|
+
actual_value=rmsd,
|
|
430
|
+
data={"rmsd": rmsd},
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
return detector
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def create_var_detector(max_var_breach: float = 0.05) -> DetectorFn:
|
|
439
|
+
"""
|
|
440
|
+
Create detector for Value-at-Risk breach (finance).
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
max_var_breach: Maximum VaR breach percentage (e.g., 0.05 = 5%)
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Detector function
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def detector(state: "PipelineState", step: int, config: Dict[str, Any]) -> Optional[AnomalyResult]:
|
|
450
|
+
var = state.metrics.get("var") or state.metrics.get("value_at_risk")
|
|
451
|
+
portfolio_value = state.metrics.get("portfolio_value", 1.0)
|
|
452
|
+
|
|
453
|
+
if var is None:
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
var_ratio = abs(var) / portfolio_value if portfolio_value > 0 else 0
|
|
457
|
+
|
|
458
|
+
if var_ratio > max_var_breach:
|
|
459
|
+
return AnomalyResult(
|
|
460
|
+
is_anomaly=True,
|
|
461
|
+
anomaly_type=AnomalyType.VAR_BREACH.value,
|
|
462
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
463
|
+
message=f"VaR breach: {var_ratio:.2%} > {max_var_breach:.2%} threshold",
|
|
464
|
+
step=step,
|
|
465
|
+
threshold=max_var_breach,
|
|
466
|
+
actual_value=var_ratio,
|
|
467
|
+
data={"var": var, "portfolio_value": portfolio_value},
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
return detector
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def create_drawdown_detector(max_drawdown: float = 0.20) -> DetectorFn:
|
|
476
|
+
"""
|
|
477
|
+
Create detector for maximum drawdown breach (finance).
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
max_drawdown: Maximum allowed drawdown (e.g., 0.20 = 20%)
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Detector function
|
|
484
|
+
"""
|
|
485
|
+
peak_value: List[float] = [0.0]
|
|
486
|
+
|
|
487
|
+
def detector(state: "PipelineState", step: int, config: Dict[str, Any]) -> Optional[AnomalyResult]:
|
|
488
|
+
portfolio_value = state.metrics.get("portfolio_value")
|
|
489
|
+
|
|
490
|
+
if portfolio_value is None:
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
# Update peak
|
|
494
|
+
if portfolio_value > peak_value[0]:
|
|
495
|
+
peak_value[0] = portfolio_value
|
|
496
|
+
|
|
497
|
+
if peak_value[0] == 0:
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
# Calculate drawdown
|
|
501
|
+
drawdown = (peak_value[0] - portfolio_value) / peak_value[0]
|
|
502
|
+
|
|
503
|
+
if drawdown > max_drawdown:
|
|
504
|
+
return AnomalyResult(
|
|
505
|
+
is_anomaly=True,
|
|
506
|
+
anomaly_type=AnomalyType.DRAWDOWN_BREACH.value,
|
|
507
|
+
severity=AnomalySeverity.CRITICAL.value,
|
|
508
|
+
message=f"Drawdown breach: {drawdown:.2%} > {max_drawdown:.2%} threshold",
|
|
509
|
+
step=step,
|
|
510
|
+
threshold=max_drawdown,
|
|
511
|
+
actual_value=drawdown,
|
|
512
|
+
data={
|
|
513
|
+
"current_value": portfolio_value,
|
|
514
|
+
"peak_value": peak_value[0],
|
|
515
|
+
"drawdown": drawdown,
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
return detector
|