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.
- aponyx/__init__.py +14 -0
- aponyx/backtest/__init__.py +31 -0
- aponyx/backtest/adapters.py +77 -0
- aponyx/backtest/config.py +84 -0
- aponyx/backtest/engine.py +560 -0
- aponyx/backtest/protocols.py +101 -0
- aponyx/backtest/registry.py +334 -0
- aponyx/backtest/strategy_catalog.json +50 -0
- aponyx/cli/__init__.py +5 -0
- aponyx/cli/commands/__init__.py +8 -0
- aponyx/cli/commands/clean.py +349 -0
- aponyx/cli/commands/list.py +302 -0
- aponyx/cli/commands/report.py +167 -0
- aponyx/cli/commands/run.py +377 -0
- aponyx/cli/main.py +125 -0
- aponyx/config/__init__.py +82 -0
- aponyx/data/__init__.py +99 -0
- aponyx/data/bloomberg_config.py +306 -0
- aponyx/data/bloomberg_instruments.json +26 -0
- aponyx/data/bloomberg_securities.json +42 -0
- aponyx/data/cache.py +294 -0
- aponyx/data/fetch.py +659 -0
- aponyx/data/fetch_registry.py +135 -0
- aponyx/data/loaders.py +205 -0
- aponyx/data/providers/__init__.py +13 -0
- aponyx/data/providers/bloomberg.py +383 -0
- aponyx/data/providers/file.py +111 -0
- aponyx/data/registry.py +500 -0
- aponyx/data/requirements.py +96 -0
- aponyx/data/sample_data.py +415 -0
- aponyx/data/schemas.py +60 -0
- aponyx/data/sources.py +171 -0
- aponyx/data/synthetic_params.json +46 -0
- aponyx/data/transforms.py +336 -0
- aponyx/data/validation.py +308 -0
- aponyx/docs/__init__.py +24 -0
- aponyx/docs/adding_data_providers.md +682 -0
- aponyx/docs/cdx_knowledge_base.md +455 -0
- aponyx/docs/cdx_overlay_strategy.md +135 -0
- aponyx/docs/cli_guide.md +607 -0
- aponyx/docs/governance_design.md +551 -0
- aponyx/docs/logging_design.md +251 -0
- aponyx/docs/performance_evaluation_design.md +265 -0
- aponyx/docs/python_guidelines.md +786 -0
- aponyx/docs/signal_registry_usage.md +369 -0
- aponyx/docs/signal_suitability_design.md +558 -0
- aponyx/docs/visualization_design.md +277 -0
- aponyx/evaluation/__init__.py +11 -0
- aponyx/evaluation/performance/__init__.py +24 -0
- aponyx/evaluation/performance/adapters.py +109 -0
- aponyx/evaluation/performance/analyzer.py +384 -0
- aponyx/evaluation/performance/config.py +320 -0
- aponyx/evaluation/performance/decomposition.py +304 -0
- aponyx/evaluation/performance/metrics.py +761 -0
- aponyx/evaluation/performance/registry.py +327 -0
- aponyx/evaluation/performance/report.py +541 -0
- aponyx/evaluation/suitability/__init__.py +67 -0
- aponyx/evaluation/suitability/config.py +143 -0
- aponyx/evaluation/suitability/evaluator.py +389 -0
- aponyx/evaluation/suitability/registry.py +328 -0
- aponyx/evaluation/suitability/report.py +398 -0
- aponyx/evaluation/suitability/scoring.py +367 -0
- aponyx/evaluation/suitability/tests.py +303 -0
- aponyx/examples/01_generate_synthetic_data.py +53 -0
- aponyx/examples/02_fetch_data_file.py +82 -0
- aponyx/examples/03_fetch_data_bloomberg.py +104 -0
- aponyx/examples/04_compute_signal.py +164 -0
- aponyx/examples/05_evaluate_suitability.py +224 -0
- aponyx/examples/06_run_backtest.py +242 -0
- aponyx/examples/07_analyze_performance.py +214 -0
- aponyx/examples/08_visualize_results.py +272 -0
- aponyx/main.py +7 -0
- aponyx/models/__init__.py +45 -0
- aponyx/models/config.py +83 -0
- aponyx/models/indicator_transformation.json +52 -0
- aponyx/models/indicators.py +292 -0
- aponyx/models/metadata.py +447 -0
- aponyx/models/orchestrator.py +213 -0
- aponyx/models/registry.py +860 -0
- aponyx/models/score_transformation.json +42 -0
- aponyx/models/signal_catalog.json +29 -0
- aponyx/models/signal_composer.py +513 -0
- aponyx/models/signal_transformation.json +29 -0
- aponyx/persistence/__init__.py +16 -0
- aponyx/persistence/json_io.py +132 -0
- aponyx/persistence/parquet_io.py +378 -0
- aponyx/py.typed +0 -0
- aponyx/reporting/__init__.py +10 -0
- aponyx/reporting/generator.py +517 -0
- aponyx/visualization/__init__.py +20 -0
- aponyx/visualization/app.py +37 -0
- aponyx/visualization/plots.py +309 -0
- aponyx/visualization/visualizer.py +242 -0
- aponyx/workflows/__init__.py +18 -0
- aponyx/workflows/concrete_steps.py +720 -0
- aponyx/workflows/config.py +122 -0
- aponyx/workflows/engine.py +279 -0
- aponyx/workflows/registry.py +116 -0
- aponyx/workflows/steps.py +180 -0
- aponyx-0.1.18.dist-info/METADATA +552 -0
- aponyx-0.1.18.dist-info/RECORD +104 -0
- aponyx-0.1.18.dist-info/WHEEL +4 -0
- aponyx-0.1.18.dist-info/entry_points.txt +2 -0
- 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)
|