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,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
|