aponyx 0.1.18__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 (104) hide show
  1. aponyx/__init__.py +14 -0
  2. aponyx/backtest/__init__.py +31 -0
  3. aponyx/backtest/adapters.py +77 -0
  4. aponyx/backtest/config.py +84 -0
  5. aponyx/backtest/engine.py +560 -0
  6. aponyx/backtest/protocols.py +101 -0
  7. aponyx/backtest/registry.py +334 -0
  8. aponyx/backtest/strategy_catalog.json +50 -0
  9. aponyx/cli/__init__.py +5 -0
  10. aponyx/cli/commands/__init__.py +8 -0
  11. aponyx/cli/commands/clean.py +349 -0
  12. aponyx/cli/commands/list.py +302 -0
  13. aponyx/cli/commands/report.py +167 -0
  14. aponyx/cli/commands/run.py +377 -0
  15. aponyx/cli/main.py +125 -0
  16. aponyx/config/__init__.py +82 -0
  17. aponyx/data/__init__.py +99 -0
  18. aponyx/data/bloomberg_config.py +306 -0
  19. aponyx/data/bloomberg_instruments.json +26 -0
  20. aponyx/data/bloomberg_securities.json +42 -0
  21. aponyx/data/cache.py +294 -0
  22. aponyx/data/fetch.py +659 -0
  23. aponyx/data/fetch_registry.py +135 -0
  24. aponyx/data/loaders.py +205 -0
  25. aponyx/data/providers/__init__.py +13 -0
  26. aponyx/data/providers/bloomberg.py +383 -0
  27. aponyx/data/providers/file.py +111 -0
  28. aponyx/data/registry.py +500 -0
  29. aponyx/data/requirements.py +96 -0
  30. aponyx/data/sample_data.py +415 -0
  31. aponyx/data/schemas.py +60 -0
  32. aponyx/data/sources.py +171 -0
  33. aponyx/data/synthetic_params.json +46 -0
  34. aponyx/data/transforms.py +336 -0
  35. aponyx/data/validation.py +308 -0
  36. aponyx/docs/__init__.py +24 -0
  37. aponyx/docs/adding_data_providers.md +682 -0
  38. aponyx/docs/cdx_knowledge_base.md +455 -0
  39. aponyx/docs/cdx_overlay_strategy.md +135 -0
  40. aponyx/docs/cli_guide.md +607 -0
  41. aponyx/docs/governance_design.md +551 -0
  42. aponyx/docs/logging_design.md +251 -0
  43. aponyx/docs/performance_evaluation_design.md +265 -0
  44. aponyx/docs/python_guidelines.md +786 -0
  45. aponyx/docs/signal_registry_usage.md +369 -0
  46. aponyx/docs/signal_suitability_design.md +558 -0
  47. aponyx/docs/visualization_design.md +277 -0
  48. aponyx/evaluation/__init__.py +11 -0
  49. aponyx/evaluation/performance/__init__.py +24 -0
  50. aponyx/evaluation/performance/adapters.py +109 -0
  51. aponyx/evaluation/performance/analyzer.py +384 -0
  52. aponyx/evaluation/performance/config.py +320 -0
  53. aponyx/evaluation/performance/decomposition.py +304 -0
  54. aponyx/evaluation/performance/metrics.py +761 -0
  55. aponyx/evaluation/performance/registry.py +327 -0
  56. aponyx/evaluation/performance/report.py +541 -0
  57. aponyx/evaluation/suitability/__init__.py +67 -0
  58. aponyx/evaluation/suitability/config.py +143 -0
  59. aponyx/evaluation/suitability/evaluator.py +389 -0
  60. aponyx/evaluation/suitability/registry.py +328 -0
  61. aponyx/evaluation/suitability/report.py +398 -0
  62. aponyx/evaluation/suitability/scoring.py +367 -0
  63. aponyx/evaluation/suitability/tests.py +303 -0
  64. aponyx/examples/01_generate_synthetic_data.py +53 -0
  65. aponyx/examples/02_fetch_data_file.py +82 -0
  66. aponyx/examples/03_fetch_data_bloomberg.py +104 -0
  67. aponyx/examples/04_compute_signal.py +164 -0
  68. aponyx/examples/05_evaluate_suitability.py +224 -0
  69. aponyx/examples/06_run_backtest.py +242 -0
  70. aponyx/examples/07_analyze_performance.py +214 -0
  71. aponyx/examples/08_visualize_results.py +272 -0
  72. aponyx/main.py +7 -0
  73. aponyx/models/__init__.py +45 -0
  74. aponyx/models/config.py +83 -0
  75. aponyx/models/indicator_transformation.json +52 -0
  76. aponyx/models/indicators.py +292 -0
  77. aponyx/models/metadata.py +447 -0
  78. aponyx/models/orchestrator.py +213 -0
  79. aponyx/models/registry.py +860 -0
  80. aponyx/models/score_transformation.json +42 -0
  81. aponyx/models/signal_catalog.json +29 -0
  82. aponyx/models/signal_composer.py +513 -0
  83. aponyx/models/signal_transformation.json +29 -0
  84. aponyx/persistence/__init__.py +16 -0
  85. aponyx/persistence/json_io.py +132 -0
  86. aponyx/persistence/parquet_io.py +378 -0
  87. aponyx/py.typed +0 -0
  88. aponyx/reporting/__init__.py +10 -0
  89. aponyx/reporting/generator.py +517 -0
  90. aponyx/visualization/__init__.py +20 -0
  91. aponyx/visualization/app.py +37 -0
  92. aponyx/visualization/plots.py +309 -0
  93. aponyx/visualization/visualizer.py +242 -0
  94. aponyx/workflows/__init__.py +18 -0
  95. aponyx/workflows/concrete_steps.py +720 -0
  96. aponyx/workflows/config.py +122 -0
  97. aponyx/workflows/engine.py +279 -0
  98. aponyx/workflows/registry.py +116 -0
  99. aponyx/workflows/steps.py +180 -0
  100. aponyx-0.1.18.dist-info/METADATA +552 -0
  101. aponyx-0.1.18.dist-info/RECORD +104 -0
  102. aponyx-0.1.18.dist-info/WHEEL +4 -0
  103. aponyx-0.1.18.dist-info/entry_points.txt +2 -0
  104. aponyx-0.1.18.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,328 @@
1
+ """
2
+ Registry for tracking suitability evaluations.
3
+
4
+ Provides CRUD operations for evaluation metadata with JSON persistence.
5
+ Follows the DataRegistry pattern for mutable state management.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, asdict, field
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from aponyx.persistence import load_json, save_json
15
+ from aponyx.evaluation.suitability.evaluator import SuitabilityResult
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class EvaluationEntry:
22
+ """
23
+ Metadata record for a suitability evaluation.
24
+
25
+ Attributes
26
+ ----------
27
+ signal_id : str
28
+ Signal name/identifier (from signal.name).
29
+ product_id : str
30
+ Product identifier matching security_id format (e.g., 'cdx_ig_5y').
31
+ evaluated_at : str
32
+ ISO timestamp of evaluation.
33
+ decision : str
34
+ Evaluation decision: "PASS", "HOLD", or "FAIL".
35
+ composite_score : float
36
+ Final composite score (0-1).
37
+ evaluator_version : str
38
+ Version of evaluator used.
39
+ report_path : str or None
40
+ Path to generated report file, if saved.
41
+ metadata : dict[str, Any]
42
+ Additional metadata (config, component scores, etc.).
43
+ """
44
+
45
+ signal_id: str
46
+ product_id: str
47
+ evaluated_at: str
48
+ decision: str
49
+ composite_score: float
50
+ evaluator_version: str
51
+ report_path: str | None = None
52
+ metadata: dict[str, Any] = field(default_factory=dict)
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ """Convert to dictionary for JSON serialization."""
56
+ return asdict(self)
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict[str, Any]) -> "EvaluationEntry":
60
+ """Create entry from dictionary."""
61
+ return cls(**data)
62
+
63
+
64
+ class SuitabilityRegistry:
65
+ """
66
+ Registry for tracking suitability evaluations.
67
+
68
+ Implements CRUD operations (create, read, update, delete) with
69
+ JSON persistence. Follows the DataRegistry pattern for mutable
70
+ state management.
71
+
72
+ Parameters
73
+ ----------
74
+ registry_path : str or Path
75
+ Path to JSON registry file.
76
+
77
+ Examples
78
+ --------
79
+ >>> from aponyx.config import SUITABILITY_REGISTRY_PATH
80
+ >>> registry = SuitabilityRegistry(SUITABILITY_REGISTRY_PATH)
81
+ >>> eval_id = registry.register_evaluation(result, "cdx_etf_basis", "cdx_ig_5y")
82
+ >>> entry = registry.get_evaluation(eval_id)
83
+ >>> evaluations = registry.list_evaluations(decision="PASS")
84
+ """
85
+
86
+ def __init__(self, registry_path: str | Path):
87
+ """
88
+ Initialize registry with JSON persistence.
89
+
90
+ Parameters
91
+ ----------
92
+ registry_path : str or Path
93
+ Path to registry JSON file.
94
+ """
95
+ self.registry_path = Path(registry_path)
96
+ self._catalog: dict[str, dict] = {}
97
+
98
+ # Load existing registry or create new
99
+ if self.registry_path.exists():
100
+ try:
101
+ self._catalog = load_json(self.registry_path)
102
+ logger.info(
103
+ "Loaded existing registry: %d evaluations",
104
+ len(self._catalog),
105
+ )
106
+ except Exception as e:
107
+ logger.warning(
108
+ "Failed to load registry from %s: %s, creating new",
109
+ self.registry_path,
110
+ e,
111
+ )
112
+ self._catalog = {}
113
+ self._save()
114
+ else:
115
+ logger.info("Creating new registry at %s", self.registry_path)
116
+ self._save()
117
+
118
+ def register_evaluation(
119
+ self,
120
+ result: SuitabilityResult,
121
+ signal_id: str,
122
+ product_id: str,
123
+ report_path: str | None = None,
124
+ evaluator_version: str = "0.1.0",
125
+ ) -> str:
126
+ """
127
+ Register new evaluation result.
128
+
129
+ Parameters
130
+ ----------
131
+ result : SuitabilityResult
132
+ Evaluation result to register.
133
+ signal_id : str
134
+ Signal identifier (typically signal.name).
135
+ product_id : str
136
+ Product identifier matching security_id format (e.g., 'cdx_ig_5y').
137
+ report_path : str, optional
138
+ Path to saved report file.
139
+ evaluator_version : str, default="0.1.0"
140
+ Version of evaluator used.
141
+
142
+ Returns
143
+ -------
144
+ str
145
+ Unique evaluation ID for retrieval.
146
+
147
+ Notes
148
+ -----
149
+ Evaluation ID format: {signal_id}_{product_id}_{timestamp}
150
+ Automatically persists registry to disk.
151
+ """
152
+ # Generate unique evaluation ID
153
+ timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
154
+ evaluation_id = f"{signal_id}_{product_id}_{timestamp_str}"
155
+
156
+ logger.debug("Registering evaluation: %s", evaluation_id)
157
+
158
+ # Create entry
159
+ entry = EvaluationEntry(
160
+ signal_id=signal_id,
161
+ product_id=product_id,
162
+ evaluated_at=result.timestamp,
163
+ decision=result.decision,
164
+ composite_score=result.composite_score,
165
+ evaluator_version=evaluator_version,
166
+ report_path=report_path,
167
+ metadata={
168
+ "component_scores": {
169
+ "data_health": result.data_health_score,
170
+ "predictive": result.predictive_score,
171
+ "economic": result.economic_score,
172
+ "stability": result.stability_score,
173
+ },
174
+ "metrics": {
175
+ "valid_obs": result.valid_obs,
176
+ "missing_pct": result.missing_pct,
177
+ "effect_size_bps": result.effect_size_bps,
178
+ },
179
+ "config": asdict(result.config),
180
+ },
181
+ )
182
+
183
+ # Add to catalog and save
184
+ self._catalog[evaluation_id] = entry.to_dict()
185
+ self._save()
186
+
187
+ logger.info(
188
+ "Registered evaluation: %s (decision=%s, score=%.3f)",
189
+ evaluation_id,
190
+ result.decision,
191
+ result.composite_score,
192
+ )
193
+
194
+ return evaluation_id
195
+
196
+ def get_evaluation(self, evaluation_id: str) -> EvaluationEntry:
197
+ """
198
+ Retrieve evaluation by ID.
199
+
200
+ Parameters
201
+ ----------
202
+ evaluation_id : str
203
+ Unique evaluation identifier.
204
+
205
+ Returns
206
+ -------
207
+ EvaluationEntry
208
+ Typed evaluation entry.
209
+
210
+ Raises
211
+ ------
212
+ KeyError
213
+ If evaluation ID not found.
214
+ """
215
+ if evaluation_id not in self._catalog:
216
+ raise KeyError(f"Evaluation not found: {evaluation_id}")
217
+
218
+ logger.debug("Retrieved evaluation: %s", evaluation_id)
219
+ return EvaluationEntry.from_dict(self._catalog[evaluation_id])
220
+
221
+ def get_evaluation_info(self, evaluation_id: str) -> dict[str, Any]:
222
+ """
223
+ Retrieve evaluation as dictionary.
224
+
225
+ Parameters
226
+ ----------
227
+ evaluation_id : str
228
+ Unique evaluation identifier.
229
+
230
+ Returns
231
+ -------
232
+ dict[str, Any]
233
+ Copy of evaluation data.
234
+
235
+ Raises
236
+ ------
237
+ KeyError
238
+ If evaluation ID not found.
239
+ """
240
+ if evaluation_id not in self._catalog:
241
+ raise KeyError(f"Evaluation not found: {evaluation_id}")
242
+
243
+ return self._catalog[evaluation_id].copy()
244
+
245
+ def list_evaluations(
246
+ self,
247
+ signal_id: str | None = None,
248
+ product_id: str | None = None,
249
+ decision: str | None = None,
250
+ ) -> list[str]:
251
+ """
252
+ List evaluations with optional filters.
253
+
254
+ Parameters
255
+ ----------
256
+ signal_id : str, optional
257
+ Filter by signal identifier.
258
+ product_id : str, optional
259
+ Filter by product identifier.
260
+ decision : str, optional
261
+ Filter by decision ("PASS", "HOLD", "FAIL").
262
+
263
+ Returns
264
+ -------
265
+ list[str]
266
+ Sorted list of evaluation IDs matching filters.
267
+
268
+ Examples
269
+ --------
270
+ >>> registry.list_evaluations() # All evaluations
271
+ >>> registry.list_evaluations(decision="PASS") # Only PASS
272
+ >>> registry.list_evaluations(signal_id="cdx_etf_basis")
273
+ """
274
+ results = []
275
+
276
+ for eval_id, info in self._catalog.items():
277
+ # Apply filters
278
+ if signal_id and info.get("signal_id") != signal_id:
279
+ continue
280
+ if product_id and info.get("product_id") != product_id:
281
+ continue
282
+ if decision and info.get("decision") != decision:
283
+ continue
284
+
285
+ results.append(eval_id)
286
+
287
+ logger.debug(
288
+ "Listed evaluations: %d total, %d matching filters",
289
+ len(self._catalog),
290
+ len(results),
291
+ )
292
+
293
+ return sorted(results)
294
+
295
+ def remove_evaluation(self, evaluation_id: str) -> None:
296
+ """
297
+ Remove evaluation from registry.
298
+
299
+ Parameters
300
+ ----------
301
+ evaluation_id : str
302
+ Unique evaluation identifier.
303
+
304
+ Raises
305
+ ------
306
+ KeyError
307
+ If evaluation ID not found.
308
+
309
+ Notes
310
+ -----
311
+ Does not delete associated report file.
312
+ Automatically persists registry to disk.
313
+ """
314
+ if evaluation_id not in self._catalog:
315
+ raise KeyError(f"Evaluation not found: {evaluation_id}")
316
+
317
+ del self._catalog[evaluation_id]
318
+ self._save()
319
+
320
+ logger.info("Removed evaluation: %s", evaluation_id)
321
+
322
+ def _save(self) -> None:
323
+ """Persist registry to JSON."""
324
+ # Ensure parent directory exists
325
+ self.registry_path.parent.mkdir(parents=True, exist_ok=True)
326
+
327
+ save_json(self._catalog, self.registry_path)
328
+ logger.debug("Saved registry to %s", self.registry_path)