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,447 @@
1
+ """
2
+ Metadata dataclasses for catalog management.
3
+
4
+ This module defines the metadata structures for indicator, transformation,
5
+ and signal definitions stored in their respective catalog JSON files.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CatalogValidationError(ValueError):
17
+ """
18
+ Validation error with structured information for catalog entries.
19
+
20
+ Attributes
21
+ ----------
22
+ catalog : str
23
+ Name of the catalog file (e.g., "signal_transformation.json")
24
+ entry : str
25
+ Name of the entry being validated
26
+ field : str
27
+ Field that failed validation
28
+ value : Any
29
+ Invalid value provided
30
+ constraint : str
31
+ Description of the constraint that was violated
32
+ suggestion : str
33
+ Suggested fix for the validation error
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ catalog: str,
39
+ entry: str,
40
+ field: str,
41
+ value: Any,
42
+ constraint: str,
43
+ suggestion: str,
44
+ ):
45
+ self.catalog = catalog
46
+ self.entry = entry
47
+ self.field = field
48
+ self.value = value
49
+ self.constraint = constraint
50
+ self.suggestion = suggestion
51
+
52
+ message = (
53
+ f"Validation failed in {catalog} entry '{entry}': "
54
+ f"field '{field}' has value '{value}'. "
55
+ f"Constraint: {constraint}. "
56
+ f"Suggestion: {suggestion}"
57
+ )
58
+ super().__init__(message)
59
+
60
+
61
+ logger = logging.getLogger(__name__)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class IndicatorMetadata:
66
+ """
67
+ Metadata for a registered indicator computation.
68
+
69
+ Indicators compute economically interpretable market metrics (spread differences,
70
+ ratios, momentum) without signal-level normalization.
71
+
72
+ Attributes
73
+ ----------
74
+ name : str
75
+ Unique indicator identifier (lowercase, underscores only).
76
+ Example: "cdx_etf_spread_diff", "spread_momentum_5d"
77
+ description : str
78
+ Human-readable explanation of economic meaning.
79
+ compute_function_name : str
80
+ Name of the compute function in indicators module.
81
+ data_requirements : dict[str, str]
82
+ Mapping from instrument types to required data fields.
83
+ Example: {"cdx": "spread", "etf": "spread"}
84
+ default_securities : dict[str, str]
85
+ Default security identifiers for each instrument type.
86
+ Example: {"cdx": "cdx_ig_5y", "etf": "lqd"}
87
+ output_units : str
88
+ Units of output values for economic interpretation.
89
+ Valid values: "basis_points", "ratio", "percentage", "index_level", "volatility_points"
90
+ parameters : dict[str, Any]
91
+ Fixed computation parameters for this indicator.
92
+ Example: {"lookback": 5, "method": "simple"}
93
+ enabled : bool
94
+ Whether indicator is available for use.
95
+ """
96
+
97
+ name: str
98
+ description: str
99
+ compute_function_name: str
100
+ data_requirements: dict[str, str]
101
+ default_securities: dict[str, str]
102
+ output_units: str
103
+ parameters: dict[str, Any]
104
+ enabled: bool = True
105
+
106
+ def __post_init__(self) -> None:
107
+ """Validate indicator metadata."""
108
+ if not self.name or not re.match(r"^[a-z][a-z0-9_]*$", self.name):
109
+ raise ValueError(
110
+ f"Indicator name must be lowercase with underscores, got: {self.name}"
111
+ )
112
+ if not self.compute_function_name:
113
+ raise ValueError("compute_function_name cannot be empty")
114
+ if not self.data_requirements:
115
+ raise ValueError(f"Indicator {self.name} has no data requirements")
116
+
117
+ # Validate output_units
118
+ valid_units = {
119
+ "basis_points",
120
+ "ratio",
121
+ "percentage",
122
+ "index_level",
123
+ "volatility_points",
124
+ }
125
+ if self.output_units not in valid_units:
126
+ raise ValueError(
127
+ f"Invalid output_units '{self.output_units}', must be one of: {valid_units}"
128
+ )
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class TransformationMetadata:
133
+ """
134
+ Metadata for a registered signal transformation.
135
+
136
+ Transformations are reusable operations (z-score, volatility adjustment, filters)
137
+ applied to indicator outputs during signal composition.
138
+
139
+ Attributes
140
+ ----------
141
+ name : str
142
+ Unique transformation identifier (lowercase, underscores only).
143
+ Example: "z_score_20d", "volatility_adjust_5d"
144
+ description : str
145
+ Human-readable explanation of transformation.
146
+ transform_type : str
147
+ Type of transformation from data.transforms module.
148
+ Valid values: "z_score", "normalized_change", "diff", "pct_change", "log_return"
149
+ parameters : dict[str, Any]
150
+ Fixed transformation parameters.
151
+ Example: {"window": 20, "min_periods": 10} for z_score
152
+ enabled : bool
153
+ Whether transformation is available for use.
154
+ """
155
+
156
+ name: str
157
+ description: str
158
+ transform_type: str
159
+ parameters: dict[str, Any]
160
+ enabled: bool = True
161
+
162
+ def __post_init__(self) -> None:
163
+ """Validate transformation metadata."""
164
+ if not self.name or not re.match(r"^[a-z][a-z0-9_]*$", self.name):
165
+ raise ValueError(
166
+ f"Transformation name must be lowercase with underscores, got: {self.name}"
167
+ )
168
+
169
+ # Validate transform_type
170
+ valid_types = {
171
+ "z_score",
172
+ "normalized_change",
173
+ "diff",
174
+ "pct_change",
175
+ "log_return",
176
+ }
177
+ if self.transform_type not in valid_types:
178
+ raise ValueError(
179
+ f"Invalid transform_type '{self.transform_type}', must be one of: {valid_types}"
180
+ )
181
+
182
+ # Validate parameters for specific transform types
183
+ if self.transform_type in ("z_score", "normalized_change"):
184
+ if "window" not in self.parameters:
185
+ raise ValueError(
186
+ f"Transformation {self.name} of type {self.transform_type} requires 'window' parameter"
187
+ )
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class SignalTransformationMetadata:
192
+ """
193
+ Metadata for signal transformation stage.
194
+
195
+ Applies trading rules to convert scores into bounded trading signals.
196
+ Operations applied in order: scale → floor/cap → neutral_range.
197
+
198
+ Attributes
199
+ ----------
200
+ name : str
201
+ Unique identifier (lowercase with underscores).
202
+ Example: "bounded_1_5", "passthrough"
203
+ description : str
204
+ Human-readable explanation of transformation behavior.
205
+ Minimum 10 characters.
206
+ scaling : float
207
+ Multiplier applied first to the score.
208
+ Must be non-zero.
209
+ Default: 1.0
210
+ floor : float | None
211
+ Lower bound after scaling.
212
+ None = no lower bound (-inf).
213
+ Must be <= cap (if both specified).
214
+ Default: None
215
+ cap : float | None
216
+ Upper bound after scaling.
217
+ None = no upper bound (+inf).
218
+ Must be >= floor (if both specified).
219
+ Default: None
220
+ neutral_range : tuple[float, float] | None
221
+ Values within [low, high] set to zero.
222
+ None = no neutral zone.
223
+ Must satisfy neutral_range[0] <= neutral_range[1].
224
+ Default: None
225
+ enabled : bool
226
+ Whether transformation is available for use.
227
+ Default: True
228
+ """
229
+
230
+ name: str
231
+ description: str
232
+ scaling: float = 1.0
233
+ floor: float | None = None
234
+ cap: float | None = None
235
+ neutral_range: tuple[float, float] | None = None
236
+ enabled: bool = True
237
+
238
+ def __post_init__(self) -> None:
239
+ """Validate signal transformation metadata."""
240
+ # Validate name format
241
+ if not self.name or not re.match(r"^[a-z][a-z0-9_]*$", self.name):
242
+ raise CatalogValidationError(
243
+ catalog="signal_transformation.json",
244
+ entry=self.name,
245
+ field="name",
246
+ value=self.name,
247
+ constraint="Name must be lowercase with underscores only (^[a-z][a-z0-9_]*$)",
248
+ suggestion="Use lowercase letters, numbers, and underscores only",
249
+ )
250
+
251
+ # Validate description
252
+ if not self.description or len(self.description) < 10:
253
+ raise CatalogValidationError(
254
+ catalog="signal_transformation.json",
255
+ entry=self.name,
256
+ field="description",
257
+ value=self.description,
258
+ constraint="Description must be at least 10 characters",
259
+ suggestion="Provide a clear description of the transformation behavior",
260
+ )
261
+
262
+ # Validate scaling is non-zero
263
+ if self.scaling == 0.0:
264
+ raise CatalogValidationError(
265
+ catalog="signal_transformation.json",
266
+ entry=self.name,
267
+ field="scaling",
268
+ value=self.scaling,
269
+ constraint="Scaling must be non-zero",
270
+ suggestion="Use scaling != 0.0 (typically 1.0 for no scaling)",
271
+ )
272
+
273
+ # Validate floor <= cap (if both specified)
274
+ if self.floor is not None and self.cap is not None and self.floor > self.cap:
275
+ raise CatalogValidationError(
276
+ catalog="signal_transformation.json",
277
+ entry=self.name,
278
+ field="floor",
279
+ value=self.floor,
280
+ constraint=f"floor must be <= cap ({self.cap})",
281
+ suggestion=f"Set floor <= {self.cap} or cap >= {self.floor}",
282
+ )
283
+
284
+ # Validate neutral_range[0] <= neutral_range[1]
285
+ if self.neutral_range is not None:
286
+ if len(self.neutral_range) != 2:
287
+ raise CatalogValidationError(
288
+ catalog="signal_transformation.json",
289
+ entry=self.name,
290
+ field="neutral_range",
291
+ value=self.neutral_range,
292
+ constraint="neutral_range must be a tuple of exactly 2 floats",
293
+ suggestion="Use [low, high] format, e.g., [-0.25, 0.25]",
294
+ )
295
+ low, high = self.neutral_range
296
+ if low > high:
297
+ raise CatalogValidationError(
298
+ catalog="signal_transformation.json",
299
+ entry=self.name,
300
+ field="neutral_range",
301
+ value=self.neutral_range,
302
+ constraint=f"neutral_range[0] ({low}) must be <= neutral_range[1] ({high})",
303
+ suggestion=f"Use [{high}, {low}] or swap the values",
304
+ )
305
+
306
+
307
+ @dataclass(frozen=True)
308
+ class SignalMetadata:
309
+ """
310
+ Metadata for a registered signal computation.
311
+
312
+ FOUR-STAGE TRANSFORMATION PIPELINE
313
+ -----------------------------------
314
+ Security → Indicator → Score → Signal → Position
315
+
316
+ Each signal references exactly one transformation from each stage (1:1:1 relationship):
317
+ 1. Indicator Transformation - Computes economic metric from securities (e.g., spread difference in bps)
318
+ 2. Score Transformation - Normalizes indicator to common scale (e.g., z-score)
319
+ 3. Signal Transformation - Applies trading rules (floor, cap, neutral_range, scaling)
320
+
321
+ This structure enables:
322
+ - Clear separation of economic logic, normalization, and trading rules
323
+ - Independent inspection of each transformation stage for debugging
324
+ - Runtime overrides at any stage without recomputing upstream stages (caching efficiency)
325
+ - Explicit specification of all transformation parameters in catalog
326
+
327
+ EXAMPLE: cdx_etf_basis signal
328
+ - Indicator: "cdx_etf_spread_diff" (basis in raw bps)
329
+ - Score: "z_score_20d" (normalize to dimensionless score)
330
+ - Signal: "passthrough" (no additional trading rules)
331
+ - Result: Tradeable signal with positive = long credit risk
332
+
333
+ Attributes
334
+ ----------
335
+ name : str
336
+ Unique signal identifier (lowercase with underscores).
337
+ Example: "cdx_etf_basis", "spread_momentum"
338
+ description : str
339
+ Human-readable description of signal purpose and logic.
340
+ Minimum 10 characters.
341
+ indicator_transformation : str
342
+ Reference to indicator_transformation.json entry (REQUIRED).
343
+ Must exist in IndicatorTransformationRegistry.
344
+ Example: "cdx_etf_spread_diff"
345
+ score_transformation : str
346
+ Reference to score_transformation.json entry (REQUIRED).
347
+ Must exist in ScoreTransformationRegistry.
348
+ Example: "z_score_20d"
349
+ signal_transformation : str
350
+ Reference to signal_transformation.json entry (REQUIRED).
351
+ Must exist in SignalTransformationRegistry.
352
+ Example: "passthrough", "bounded_1_5"
353
+ enabled : bool
354
+ Whether signal should be included in computation.
355
+ Default: True
356
+ sign_multiplier : int
357
+ Multiplier to apply to final signal output for sign convention alignment.
358
+ Positive signal = long credit risk (buy CDX).
359
+ Use -1 to invert signals that naturally produce opposite signs.
360
+ Must be -1 or 1.
361
+ Default: 1 (no inversion)
362
+
363
+ Notes
364
+ -----
365
+ All three transformation references are MANDATORY (no defaults).
366
+ Signals must explicitly specify all stages of the transformation pipeline.
367
+
368
+ Runtime overrides (via WorkflowConfig):
369
+ - indicator_transformation_override: Swap indicator while keeping score/signal transformations
370
+ - score_transformation_override: Swap score transformation while keeping indicator/signal
371
+ - signal_transformation_override: Swap signal transformation while keeping indicator/score
372
+ - security_mapping: Override which securities to load for indicator data requirements
373
+ """
374
+
375
+ name: str
376
+ description: str
377
+ indicator_transformation: str
378
+ score_transformation: str
379
+ signal_transformation: str
380
+ enabled: bool = True
381
+ sign_multiplier: int = 1
382
+
383
+ def __post_init__(self) -> None:
384
+ """Validate signal metadata."""
385
+ # Validate name format
386
+ if not self.name or not re.match(r"^[a-z][a-z0-9_]*$", self.name):
387
+ raise CatalogValidationError(
388
+ catalog="signal_catalog.json",
389
+ entry=self.name,
390
+ field="name",
391
+ value=self.name,
392
+ constraint="Name must be lowercase with underscores only (^[a-z][a-z0-9_]*$)",
393
+ suggestion="Use lowercase letters, numbers, and underscores only",
394
+ )
395
+
396
+ # Validate description
397
+ if not self.description or len(self.description) < 10:
398
+ raise CatalogValidationError(
399
+ catalog="signal_catalog.json",
400
+ entry=self.name,
401
+ field="description",
402
+ value=self.description,
403
+ constraint="Description must be at least 10 characters",
404
+ suggestion="Provide a clear description of signal purpose and logic",
405
+ )
406
+
407
+ # Enforce explicit transformation references (REQUIRED, no defaults)
408
+ if not self.indicator_transformation:
409
+ raise CatalogValidationError(
410
+ catalog="signal_catalog.json",
411
+ entry=self.name,
412
+ field="indicator_transformation",
413
+ value=self.indicator_transformation,
414
+ constraint="indicator_transformation is required (cannot be empty)",
415
+ suggestion="Specify an indicator from indicator_transformation.json",
416
+ )
417
+
418
+ if not self.score_transformation:
419
+ raise CatalogValidationError(
420
+ catalog="signal_catalog.json",
421
+ entry=self.name,
422
+ field="score_transformation",
423
+ value=self.score_transformation,
424
+ constraint="score_transformation is required (cannot be empty)",
425
+ suggestion="Specify a transformation from score_transformation.json",
426
+ )
427
+
428
+ if not self.signal_transformation:
429
+ raise CatalogValidationError(
430
+ catalog="signal_catalog.json",
431
+ entry=self.name,
432
+ field="signal_transformation",
433
+ value=self.signal_transformation,
434
+ constraint="signal_transformation is required (cannot be empty)",
435
+ suggestion="Specify a transformation from signal_transformation.json (e.g., 'passthrough')",
436
+ )
437
+
438
+ # Validate sign_multiplier is ±1
439
+ if self.sign_multiplier not in (-1, 1):
440
+ raise CatalogValidationError(
441
+ catalog="signal_catalog.json",
442
+ entry=self.name,
443
+ field="sign_multiplier",
444
+ value=self.sign_multiplier,
445
+ constraint="sign_multiplier must be -1 or 1",
446
+ suggestion="Use 1 (no inversion) or -1 (invert sign)",
447
+ )
@@ -0,0 +1,213 @@
1
+ """
2
+ Signal computation orchestration using registry pattern.
3
+
4
+ This module orchestrates batch signal computation from the signal catalog.
5
+ It coordinates between signal metadata (registry.py, metadata.py) and
6
+ signal composition (signal_composer.py) via the indicator + transformation pattern.
7
+
8
+ Design Notes
9
+ ------------
10
+ market_data dict pattern:
11
+ The orchestrator accepts a dict mapping generic keys (e.g., "cdx", "etf")
12
+ to DataFrame objects. This enables catalog-driven computation where:
13
+
14
+ 1. Different signals require different data combinations
15
+ 2. Indicators define requirements declaratively via data_requirements
16
+ 3. Orchestrator resolves data dynamically for indicator computation
17
+
18
+ Alternative approaches considered:
19
+ - Named parameters: Inflexible, requires knowing all data types upfront
20
+ - Auto-loading from DataRegistry: Couples signal computation to data loading
21
+
22
+ The dict pattern is kept for flexibility despite adding indirection.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from typing import TYPE_CHECKING
29
+
30
+ import pandas as pd
31
+
32
+ from .metadata import SignalMetadata
33
+
34
+ if TYPE_CHECKING:
35
+ from .registry import SignalRegistry
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def compute_registered_signals(
41
+ registry: SignalRegistry,
42
+ market_data: dict[str, pd.DataFrame],
43
+ ) -> dict[str, pd.Series]:
44
+ """
45
+ Compute all enabled signals from registry using provided market data.
46
+
47
+ Validates data requirements and executes signal compositions via the
48
+ indicator + transformation pattern.
49
+
50
+ Correct Usage Pattern
51
+ ---------------------
52
+ 1. Get all required data keys: `aponyx.data.requirements.get_required_data_keys()`
53
+ 2. Load all required data into market_data dict
54
+ 3. Compute all enabled signals at once with this function
55
+ 4. Select individual signals for evaluation/backtesting
56
+
57
+ This batch computation approach is efficient because:
58
+ - Data is loaded once (not per-signal)
59
+ - All signals computed in single pass
60
+ - Results can be cached/reused for different analyses
61
+
62
+ Parameters
63
+ ----------
64
+ registry : SignalRegistry
65
+ Signal registry containing metadata and catalog.
66
+ market_data : dict[str, pd.DataFrame]
67
+ Market data mapping. Keys should match indicator data_requirements.
68
+ Must contain ALL data keys required by ANY enabled signal.
69
+ Example: {"cdx": cdx_df, "etf": etf_df, "vix": vix_df}
70
+
71
+ The dict pattern enables catalog-driven computation where different
72
+ signals can specify different data requirements without hardcoding.
73
+
74
+ Returns
75
+ -------
76
+ dict[str, pd.Series]
77
+ Mapping from signal name to computed signal series.
78
+ Contains one entry per enabled signal in the registry.
79
+
80
+ Raises
81
+ ------
82
+ ValueError
83
+ If required market data is missing or lacks required columns.
84
+
85
+ Examples
86
+ --------
87
+ Correct pattern (load all data once, compute all signals):
88
+
89
+ >>> from aponyx.config import SIGNAL_CATALOG_PATH
90
+ >>> from aponyx.data.requirements import get_required_data_keys
91
+ >>> from aponyx.models import SignalRegistry, compute_registered_signals
92
+ >>>
93
+ >>> # 1. Get required data keys from catalog
94
+ >>> required_keys = get_required_data_keys(SIGNAL_CATALOG_PATH) # {"cdx", "etf", "vix"}
95
+ >>>
96
+ >>> # 2. Load all required data once
97
+ >>> market_data = {}
98
+ >>> for key in required_keys:
99
+ ... market_data[key] = load_data_for(key)
100
+ >>>
101
+ >>> # 3. Compute all enabled signals
102
+ >>> registry = SignalRegistry(SIGNAL_CATALOG_PATH)
103
+ >>> all_signals = compute_registered_signals(registry, market_data)
104
+ >>>
105
+ >>> # 4. Use individual signals for analysis
106
+ >>> basis_signal = all_signals["cdx_etf_basis"]
107
+ >>> gap_signal = all_signals["cdx_vix_gap"]
108
+
109
+ Notes
110
+ -----
111
+ The market_data dict keys must match the keys in each indicator's
112
+ data_requirements field from the catalog. For example, if an indicator
113
+ specifies {"cdx": "spread", "vix": "level"}, then market_data must
114
+ contain keys "cdx" and "vix" with DataFrames having those columns.
115
+
116
+ Use aponyx.data.requirements.get_required_data_keys() to determine
117
+ what data to load before calling this function.
118
+ """
119
+ enabled_signals = registry.get_enabled()
120
+
121
+ logger.info(
122
+ "Computing %d enabled signals: %s",
123
+ len(enabled_signals),
124
+ ", ".join(sorted(enabled_signals.keys())),
125
+ )
126
+
127
+ results: dict[str, pd.Series] = {}
128
+
129
+ for signal_name, metadata in enabled_signals.items():
130
+ try:
131
+ signal_series = _compute_signal(metadata, market_data)
132
+ results[signal_name] = signal_series
133
+
134
+ logger.debug(
135
+ "Computed signal '%s': valid_obs=%d",
136
+ signal_name,
137
+ signal_series.notna().sum(),
138
+ )
139
+
140
+ except Exception as e:
141
+ logger.error(
142
+ "Failed to compute signal '%s': %s",
143
+ signal_name,
144
+ e,
145
+ exc_info=True,
146
+ )
147
+ raise
148
+
149
+ logger.info("Successfully computed %d signals", len(results))
150
+ return results
151
+
152
+
153
+ def _compute_signal(
154
+ metadata: SignalMetadata,
155
+ market_data: dict[str, pd.DataFrame],
156
+ ) -> pd.Series:
157
+ """
158
+ Compute a single signal using four-stage transformation pipeline.
159
+
160
+ Applies sign multiplier from catalog metadata (already applied in compose_signal).
161
+
162
+ Parameters
163
+ ----------
164
+ metadata : SignalMetadata
165
+ Signal metadata with indicator_transformation, score_transformation, signal_transformation.
166
+ market_data : dict[str, pd.DataFrame]
167
+ Available market data.
168
+
169
+ Returns
170
+ -------
171
+ pd.Series
172
+ Computed signal (sign multiplier already applied).
173
+
174
+ Raises
175
+ ------
176
+ ValueError
177
+ If required data is missing or lacks required columns.
178
+ """
179
+ from ..config import (
180
+ INDICATOR_TRANSFORMATION_PATH,
181
+ SCORE_TRANSFORMATION_PATH,
182
+ SIGNAL_CATALOG_PATH,
183
+ SIGNAL_TRANSFORMATION_PATH,
184
+ )
185
+ from .registry import (
186
+ IndicatorTransformationRegistry,
187
+ ScoreTransformationRegistry,
188
+ SignalRegistry,
189
+ SignalTransformationRegistry,
190
+ )
191
+ from .signal_composer import compose_signal
192
+
193
+ # Lazy-load registries
194
+ indicator_registry = IndicatorTransformationRegistry(INDICATOR_TRANSFORMATION_PATH)
195
+ score_registry = ScoreTransformationRegistry(SCORE_TRANSFORMATION_PATH)
196
+ signal_transformation_registry = SignalTransformationRegistry(
197
+ SIGNAL_TRANSFORMATION_PATH
198
+ )
199
+ signal_registry = SignalRegistry(SIGNAL_CATALOG_PATH)
200
+
201
+ # Compose signal using four-stage pipeline
202
+ # (sign multiplier already applied within compose_signal)
203
+ signal = compose_signal(
204
+ signal_name=metadata.name,
205
+ market_data=market_data,
206
+ indicator_registry=indicator_registry,
207
+ score_registry=score_registry,
208
+ signal_transformation_registry=signal_transformation_registry,
209
+ signal_registry=signal_registry,
210
+ include_intermediates=False,
211
+ )
212
+
213
+ return signal