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,551 @@
1
+ # Governance Design — aponyx Framework
2
+
3
+ **Purpose:**
4
+ This document describes the governance architecture for the *aponyx* research framework. It explains the design principles, patterns, and implementation examples that maintain consistency and modularity across the system.
5
+
6
+ ---
7
+
8
+ ## 1. Design Overview
9
+
10
+ The governance system exists to:
11
+ - Provide **structure without friction** for researchers.
12
+ - Keep **layers isolated yet interoperable** through clear conventions.
13
+ - Ensure **consistency and reproducibility** across data, models, and backtests.
14
+
15
+ The framework organizes governance around three pillars:
16
+
17
+ | Pillar | Scope | Persistence | Description |
18
+ |--------|--------|--------------|--------------|
19
+ | **Config** | Paths, constants, defaults | Hardcoded in `config/` | Declares static project settings and directory locations. |
20
+ | **Registry** | Data and metadata tracking | JSON file | Tracks datasets and their lineage. |
21
+ | **Catalog** | Model and signal definitions | JSON file | Declares signals and computational assets available for research. |
22
+
23
+ Each pillar owns a single JSON or constant source of truth.
24
+
25
+ ---
26
+
27
+ ## 2. Design Principles
28
+
29
+ 1. **Single Source of Truth per Concern**
30
+ Each governance domain (config, registry, catalog) has exactly one canonical file.
31
+
32
+ 2. **Flat, Readable Metadata**
33
+ All metadata should be inspectable and editable by hand in JSON format.
34
+
35
+ 3. **Minimal State**
36
+ Governance components prefer functional patterns where practical. Class-based registries are acceptable when they provide clear lifecycle management (load → validate → query → save). State should be immutable after initialization (except DataRegistry which supports CRUD operations).
37
+
38
+ 4. **Pure Dependencies**
39
+ Governance modules never import from higher layers (e.g., models or backtest).
40
+
41
+ 5. **Determinism**
42
+ Loading a governance object from disk must always yield the same in-memory representation.
43
+
44
+ 6. **Replaceability**
45
+ Governance constructs are designed to be replaceable without rewriting other layers.
46
+
47
+ 7. **Convention over Abstraction**
48
+ Uses naming and directory conventions instead of frameworks or inheritance.
49
+
50
+ ---
51
+
52
+ ## 3. Governance Pillars
53
+
54
+ ### 3.1 Configuration (`config/`)
55
+
56
+ - Declares constant paths, project root, and cache directories.
57
+ - Must be deterministic at import time.
58
+ - No dynamic configuration or environment-variable logic.
59
+ - Used by all layers for locating data, cache, and registries.
60
+
61
+ ### 3.2 Registry (`data/registry.py`)
62
+
63
+ - Tracks datasets produced or consumed by the framework.
64
+ - Maintains lightweight metadata such as instrument, source, and date range.
65
+ - Each dataset is uniquely identified by name and stored in a single JSON registry.
66
+ - Supports basic operations: register, lookup, list.
67
+
68
+ ### 3.3 Catalog (`models/registry.py`, `models/signal_catalog.json`)
69
+
70
+ - Enumerates all available signal definitions.
71
+ - Each entry specifies: name, description, data requirements, and whether it is enabled.
72
+ - The catalog acts as the research “menu” from which signals are selected for computation.
73
+ - Catalog edits are manual and version-controlled.
74
+
75
+ ---
76
+
77
+ ## 4. Governance Lifecycle Pattern
78
+
79
+ 1. **Load** from a static source (constants or JSON file).
80
+ 2. **Inspect** or query (e.g., list enabled signals).
81
+ 3. **Use** in downstream processes (fetching data, computing signals).
82
+ 4. **Optionally Save** if new metadata is produced.
83
+
84
+ ---
85
+
86
+ ## 5. Layer Boundaries
87
+
88
+ | Layer | May Import From | Must Not Import From |
89
+ |-------|-----------------|----------------------|
90
+ | `config/` | None | All others |
91
+ | `persistence/` | `config` | `data`, `models`, `backtest` |
92
+ | `data/` | `config`, `persistence`, `data` (own modules) | `models`, `backtest`, `visualization` |
93
+ | `models/` | `config`, `data` (schemas only) | `backtest`, `visualization` |
94
+ | `backtest/` | `config`, `models` | `data`, `visualization` |
95
+ | `visualization/` | None | All others |
96
+
97
+ ---
98
+
99
+ ## 6. Implementation Patterns
100
+
101
+ ### 6.1 Config — Import-Time Constants
102
+
103
+ **File:** `src/aponyx/config/__init__.py`
104
+
105
+ ```python
106
+ from pathlib import Path
107
+ from typing import Final
108
+
109
+ # Project root and data directories
110
+ PROJECT_ROOT: Final[Path] = Path(__file__).parent.parent.parent.parent
111
+ DATA_DIR: Final[Path] = PROJECT_ROOT / "data"
112
+ LOGS_DIR: Final[Path] = PROJECT_ROOT / "logs"
113
+
114
+ # Cache configuration
115
+ CACHE_ENABLED: Final[bool] = True
116
+ CACHE_TTL_DAYS: Final[int] = 1
117
+ CACHE_DIR: Final[Path] = DATA_DIR / "cache"
118
+
119
+ # Catalog paths
120
+ INDICATOR_TRANSFORMATION_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/models/indicator_transformation.json"
121
+ SCORE_TRANSFORMATION_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/models/score_transformation.json"
122
+ SIGNAL_CATALOG_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/models/signal_catalog.json"
123
+ STRATEGY_CATALOG_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/backtest/strategy_catalog.json"
124
+
125
+ # Bloomberg configuration paths
126
+ BLOOMBERG_SECURITIES_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/data/bloomberg_securities.json"
127
+ BLOOMBERG_INSTRUMENTS_PATH: Final[Path] = PROJECT_ROOT / "src/aponyx/data/bloomberg_instruments.json"
128
+ ```
129
+
130
+ **Usage:**
131
+ ```python
132
+ from aponyx.config import SIGNAL_CATALOG_PATH, DATA_DIR, CACHE_ENABLED
133
+
134
+ # Config values are available immediately at import time
135
+ if CACHE_ENABLED:
136
+ cache_path = DATA_DIR / "cache" / "cdx_data.parquet"
137
+ ```
138
+
139
+ **Pattern:** Import-time constants with `Final` type hints. No class instantiation required.
140
+
141
+ ---
142
+
143
+ ### 6.2 DataRegistry — Class-Based Registry
144
+
145
+ **File:** `src/aponyx/data/registry.py`
146
+
147
+ **Lifecycle:**
148
+
149
+ ```python
150
+ from aponyx.config import REGISTRY_PATH, DATA_DIR
151
+ from aponyx.data.registry import DataRegistry
152
+
153
+ # 1. LOAD: Instantiate registry (loads JSON)
154
+ registry = DataRegistry(REGISTRY_PATH, DATA_DIR)
155
+
156
+ # 2. INSPECT: Query registered datasets
157
+ all_datasets = registry.list_datasets()
158
+ cdx_datasets = registry.list_datasets(instrument="CDX.NA.IG")
159
+
160
+ # 3. USE: Retrieve metadata for data loading
161
+ info = registry.get_dataset_info("cdx_ig_5y")
162
+ file_path = info["file_path"]
163
+ start_date = info["start_date"]
164
+
165
+ # 4. SAVE: Register new dataset (auto-saves)
166
+ registry.register_dataset(
167
+ name="new_cdx_data",
168
+ file_path="data/raw/cdx_new.parquet",
169
+ instrument="CDX.NA.IG",
170
+ )
171
+ ```
172
+
173
+ **Pattern:** Class-based registry with state (`self._catalog`). JSON persistence via `_save()` method. Supports CRUD operations.
174
+
175
+ ---
176
+
177
+ ### 6.3 IndicatorTransformationRegistry — Class-Based Catalog for Market Indicators
178
+
179
+ **Files:**
180
+ - `src/aponyx/models/metadata.py` — IndicatorMetadata dataclass
181
+ - `src/aponyx/models/registry.py` — IndicatorTransformationRegistry class
182
+ - `src/aponyx/models/indicators.py` — Indicator compute functions
183
+
184
+ **Lifecycle:**
185
+
186
+ ```python
187
+ from aponyx.config import INDICATOR_TRANSFORMATION_PATH
188
+ from aponyx.models import IndicatorTransformationRegistry, compute_indicator
189
+
190
+ # 1. LOAD: Instantiate registry (loads + validates JSON)
191
+ registry = IndicatorTransformationRegistry(INDICATOR_TRANSFORMATION_PATH)
192
+ # Validation happens automatically in __init__:
193
+ # - Checks all compute functions exist in indicators module
194
+ # - Validates output_units and data_requirements
195
+ # - Raises ValueError if function missing or validation fails
196
+
197
+ # 2. INSPECT: Query indicator metadata
198
+ enabled = registry.get_enabled() # Only enabled indicators
199
+ all_indicators = registry.list_all() # All indicators
200
+ metadata = registry.get_metadata("cdx_etf_spread_diff")
201
+
202
+ # 3. USE: Compute indicator with caching
203
+ market_data = {"cdx": cdx_df, "etf": etf_df}
204
+ indicator = compute_indicator(
205
+ indicator_metadata=metadata,
206
+ market_data=market_data,
207
+ use_cache=True # Cached at data/cache/indicators/
208
+ )
209
+
210
+ # 4. DEPENDENCY TRACKING: Query which signals use an indicator
211
+ dependent_signals = registry.get_dependent_signals("cdx_etf_spread_diff")
212
+ ```
213
+
214
+ **Pattern:**
215
+ - Class-based registry with fail-fast validation
216
+ - Indicators output economically interpretable values (basis_points, ratio, percentage)
217
+ - Caching via hash-based file names in `data/cache/indicators/`
218
+ - Dependency tracking for impact analysis
219
+
220
+ ---
221
+
222
+ ### 6.4 ScoreTransformationRegistry — Class-Based Catalog for Signal Transformations
223
+
224
+ **Files:**
225
+ - `src/aponyx/models/metadata.py` — TransformationMetadata dataclass
226
+ - `src/aponyx/models/registry.py` — ScoreTransformationRegistry class
227
+ - `src/aponyx/models/transformations.py` — Transformation functions
228
+
229
+ **Lifecycle:**
230
+
231
+ ```python
232
+ from aponyx.config import SCORE_TRANSFORMATION_PATH
233
+ from aponyx.models import ScoreTransformationRegistry, apply_signal_transformation
234
+
235
+ # 1. LOAD: Instantiate registry (loads + validates JSON)
236
+ registry = ScoreTransformationRegistry(SCORE_TRANSFORMATION_PATH)
237
+
238
+ # 2. INSPECT: Query transformation metadata
239
+ all_transforms = registry.list_all()
240
+ metadata = registry.get_metadata("z_score_20d")
241
+
242
+ # 3. USE: Apply transformation to indicator
243
+ transformed = apply_signal_transformation(
244
+ data=indicator,
245
+ transformation_metadata=metadata
246
+ )
247
+ ```
248
+
249
+ **Pattern:**
250
+ - Class-based registry for reusable transformations
251
+ - Transformations are pure functions (z-score, volatility adjustment, etc.)
252
+ - Parameters defined in catalog (window, min_periods, etc.)
253
+ - Applied sequentially in signal composition
254
+
255
+ ---
256
+
257
+ ### 6.5 SignalRegistry — Class-Based Catalog with Indicator Dependencies
258
+
259
+ **Files:**
260
+ - `src/aponyx/models/metadata.py` — SignalMetadata dataclass
261
+ - `src/aponyx/models/registry.py` — SignalRegistry class
262
+ - `src/aponyx/models/signal_composer.py` — compose_signal() function
263
+ - `src/aponyx/models/orchestrator.py` — compute_registered_signals() function
264
+
265
+ **Lifecycle:**
266
+
267
+ ```python
268
+ from aponyx.config import SIGNAL_CATALOG_PATH, INDICATOR_TRANSFORMATION_PATH, SCORE_TRANSFORMATION_PATH
269
+ from aponyx.models import SignalRegistry, IndicatorTransformationRegistry, ScoreTransformationRegistry
270
+ from aponyx.models import compose_signal, compute_registered_signals
271
+
272
+ # 1. LOAD: Instantiate registries (loads + validates JSON)
273
+ signal_registry = SignalRegistry(SIGNAL_CATALOG_PATH)
274
+ indicator_registry = IndicatorTransformationRegistry(INDICATOR_TRANSFORMATION_PATH)
275
+ transformation_registry = ScoreTransformationRegistry(SCORE_TRANSFORMATION_PATH)
276
+ # Validation happens automatically in __init__:
277
+ # - Signals MUST have indicator_dependencies and transformations fields
278
+ # - No compute_function_name, data_requirements, or arg_mapping allowed
279
+ # - Raises ValueError if schema violated
280
+
281
+ # 2. INSPECT: Query signal metadata
282
+ enabled = signal_registry.get_enabled() # Only enabled signals
283
+ all_signals = signal_registry.list_all() # All signals
284
+ metadata = signal_registry.get_metadata("cdx_etf_basis")
285
+
286
+ # 3. USE: Compose signal from indicator + transformation
287
+ market_data = {"cdx": cdx_df, "etf": etf_df}
288
+ signal = compose_signal(
289
+ signal_metadata=metadata,
290
+ market_data=market_data,
291
+ indicator_registry=indicator_registry,
292
+ transformation_registry=transformation_registry
293
+ )
294
+
295
+ # Batch computation of all enabled signals
296
+ signals = compute_registered_signals(
297
+ signal_registry, market_data, indicator_registry, transformation_registry
298
+ )
299
+
300
+ # 4. SAVE: Update catalog (optional)
301
+ # registry.save_catalog() # Overwrites original JSON
302
+ ```
303
+
304
+ **Pattern:**
305
+ - Class-based registry with fail-fast validation
306
+ - Signals reference indicators (no embedded computation logic)
307
+ - Signals reference transformations (applied sequentially)
308
+ - Schema enforcement via SignalMetadata.__post_init__
309
+ - Orchestration bridges three registries
310
+
311
+ **Module Organization:**
312
+ ```
313
+ models/
314
+ metadata.py # IndicatorMetadata, TransformationMetadata, SignalMetadata
315
+ registry.py # IndicatorTransformationRegistry, ScoreTransformationRegistry, SignalRegistry
316
+ indicators.py # Indicator compute functions
317
+ transformations.py # Transformation functions
318
+ signal_composer.py # compose_signal() - combines indicators + transformations
319
+ orchestrator.py # compute_registered_signals() - batch computation
320
+ config.py # IndicatorConfig, TransformationConfig
321
+ ```
322
+
323
+ This separation clarifies responsibilities:
324
+ - `metadata.py` = data structure definitions
325
+ - `registry.py` = catalog lifecycle management for all three layers
326
+ - `indicators.py` = economically interpretable market metrics
327
+ - `transformations.py` = signal processing operations
328
+ - `signal_composer.py` = signal composition logic
329
+ - `orchestrator.py` = batch orchestration
330
+
331
+ ---
332
+
333
+ ### 6.6 PerformanceRegistry — Class-Based Catalog for Performance Evaluations
334
+
335
+ **File:** `src/aponyx/evaluation/performance/registry.py`
336
+
337
+ **Lifecycle:**
338
+
339
+ ```python
340
+ from aponyx.config import PERFORMANCE_REGISTRY_PATH
341
+ from aponyx.evaluation.performance import PerformanceRegistry
342
+
343
+ # 1. LOAD: Instantiate registry (loads JSON)
344
+ registry = PerformanceRegistry(PERFORMANCE_REGISTRY_PATH)
345
+
346
+ # 2. INSPECT: Query evaluation metadata
347
+ all_evals = registry.list_evaluations()
348
+ cdx_evals = registry.list_evaluations(signal_id="cdx_etf_basis")
349
+ balanced_evals = registry.list_evaluations(strategy_id="balanced")
350
+
351
+ # 3. USE: Register new performance evaluation
352
+ eval_id = registry.register_evaluation(
353
+ performance_result=perf_result,
354
+ signal_id="cdx_etf_basis",
355
+ strategy_id="balanced",
356
+ report_path=Path("data/workflows/cdx_etf_basis_balanced_20251109_120000/reports/cdx_etf_basis_balanced_20251109.md"),
357
+ )
358
+
359
+ # 4. RETRIEVE: Get specific evaluation metadata
360
+ metadata = registry.get_evaluation(eval_id)
361
+
362
+ # 5. SAVE: Auto-saves on register (manual save also available)
363
+ registry.save_catalog()
364
+ ```
365
+
366
+ **Pattern:** Class-based registry similar to SuitabilityRegistry. Tracks performance evaluation runs with comprehensive metadata including signal, strategy, stability score, and report paths.
367
+
368
+ ---
369
+
370
+ ### 6.7 StrategyRegistry — Class-Based Catalog for Backtest Strategies
371
+
372
+ **File:** `src/aponyx/backtest/registry.py`
373
+
374
+ **Lifecycle:**
375
+
376
+ ```python
377
+ from aponyx.config import STRATEGY_CATALOG_PATH
378
+ from aponyx.backtest import StrategyRegistry, run_backtest
379
+
380
+ # 1. LOAD: Instantiate registry (loads + validates JSON)
381
+ registry = StrategyRegistry(STRATEGY_CATALOG_PATH)
382
+ # Validation happens automatically in __init__:
383
+ # - Validates position_size_mm > 0
384
+ # - Validates sizing_mode is 'binary' or 'proportional'
385
+ # - Validates stop_loss_pct/take_profit_pct in (0, 100] if set
386
+
387
+ # 2. INSPECT: Query strategy metadata
388
+ enabled = registry.get_enabled() # Only enabled strategies
389
+ metadata = registry.get_metadata("balanced")
390
+
391
+ # 3. USE: Convert strategy to BacktestConfig
392
+ config = metadata.to_config(
393
+ position_size_mm_override=20.0, # Override default
394
+ )
395
+
396
+ # Run backtest with strategy config
397
+ result = run_backtest(signal_series, cdx_spread, config)
398
+
399
+ # 4. SAVE: Update catalog (optional, not typical)
400
+ # registry.save_catalog()
401
+ ```
402
+
403
+ **Pattern:** Class-based registry similar to SignalRegistry. `StrategyMetadata.to_config()` bridges catalog to runtime config dataclass.
404
+
405
+ ---
406
+
407
+ ### 6.6 Bloomberg Config — Functional Pattern with Module-Level Caching
408
+
409
+ **File:** `src/aponyx/data/bloomberg_config.py`
410
+
411
+ **Lifecycle:**
412
+
413
+ ```python
414
+ from aponyx.data.bloomberg_config import (
415
+ get_instrument_spec,
416
+ get_security_spec,
417
+ get_bloomberg_ticker,
418
+ )
419
+
420
+ # 1. LOAD: Automatic lazy-loading on first access
421
+ # JSON files loaded into module-level variables on first function call
422
+
423
+ # 2. INSPECT: Query specifications
424
+ instrument_spec = get_instrument_spec("CDX")
425
+ security_spec = get_security_spec("CDX.NA.IG", "5Y")
426
+
427
+ # 3. USE: Resolve ticker for data fetching
428
+ ticker = get_bloomberg_ticker("CDX.NA.IG", "5Y")
429
+ # Returns: "CDX IG CDSI 5Y Corp"
430
+
431
+ # 4. SAVE: Read-only (no save operation)
432
+ # Bloomberg config is managed manually via JSON files
433
+ ```
434
+
435
+ **Pattern:** Functional pattern with module-level caching (`_INSTRUMENTS_CATALOG`, `_SECURITIES_CATALOG`). Pure functions, no class instantiation. Lazy-loads JSON on first access.
436
+
437
+ ---
438
+
439
+ ### 6.10 Pattern Comparison
440
+
441
+ | Pillar | Implementation | State | Validation | Save Support |
442
+ |--------|---------------|-------|------------|--------------|
443
+ | **Config** | Import-time constants | None | N/A | No |
444
+ | **DataRegistry** | Class-based | Mutable (`self._catalog`) | On save | Yes |
445
+ | **IndicatorTransformationRegistry** | Class-based | Immutable (frozen dataclass) | Fail-fast (load time) | Yes |
446
+ | **ScoreTransformationRegistry** | Class-based | Immutable (frozen dataclass) | Fail-fast (load time) | Yes |
447
+ | **SignalRegistry** | Class-based | Immutable (frozen dataclass) | Fail-fast (load time) | Yes |
448
+ | **StrategyRegistry** | Class-based | Immutable (frozen dataclass) | Fail-fast (load time) | Yes |
449
+ | **SuitabilityRegistry** | Class-based | Mutable (`self._evaluations`) | On register | Yes |
450
+ | **PerformanceRegistry** | Class-based | Mutable (`self._evaluations`) | On register | Yes |
451
+ | **Bloomberg Config** | Functional | Module-level cache | On access | No |
452
+
453
+ **When to use each pattern:**
454
+
455
+ - **Import-time constants:** Static configuration that never changes (paths, flags)
456
+ - **Class-based registry:** Needs CRUD operations or mutable state (DataRegistry)
457
+ - **Class-based catalog:** Needs validation + orchestration (IndicatorTransformationRegistry, ScoreTransformationRegistry, SignalRegistry, StrategyRegistry)
458
+ - **Functional pattern:** Read-only lookup with lazy loading (Bloomberg config)
459
+
460
+ **Key insight:** Both class-based and functional patterns satisfy the governance spine. Choose based on:
461
+ 1. **Mutability needs:** Mutable state → class-based
462
+ 2. **Validation complexity:** Fail-fast validation → class-based with `__post_init__`
463
+ 3. **Simplicity:** Read-only lookups → functional
464
+
465
+ ---
466
+
467
+ ## 7. Fail-Fast Validation
468
+
469
+ ### IndicatorTransformationRegistry Validation
470
+
471
+ ```python
472
+ def _validate_catalog(self) -> None:
473
+ """Validate that all indicator compute functions exist in indicators module."""
474
+ for name, metadata in self._indicators.items():
475
+ if not hasattr(indicators, metadata.compute_function_name):
476
+ raise ValueError(
477
+ f"Indicator '{name}' references non-existent compute function: "
478
+ f"{metadata.compute_function_name}"
479
+ )
480
+ ```
481
+
482
+ **Timing:** Called at end of `_load_catalog()` before registry initialization completes.
483
+
484
+ **Benefits:**
485
+ - Catches typos in function names immediately
486
+ - Prevents runtime failures during indicator computation
487
+ - Clear error messages with indicator name and missing function
488
+
489
+ ### SignalRegistry Validation
490
+
491
+ ```python
492
+ def _validate_catalog(self) -> None:
493
+ """Validate that all signals have required fields and reference valid indicators."""
494
+ for name, metadata in self._signals.items():
495
+ # Enforce schema: signals MUST have indicator_dependencies and transformations
496
+ if not metadata.indicator_dependencies:
497
+ raise ValueError(
498
+ f"Signal '{name}' missing required field: indicator_dependencies"
499
+ )
500
+ if not metadata.transformations:
501
+ raise ValueError(
502
+ f"Signal '{name}' missing required field: transformations"
503
+ )
504
+
505
+ # Reject legacy fields
506
+ if hasattr(metadata, 'compute_function_name'):
507
+ raise ValueError(
508
+ f"Signal '{name}' uses deprecated field 'compute_function_name'. "
509
+ "Signals must reference indicators via indicator_dependencies."
510
+ )
511
+ ```
512
+
513
+ **Timing:** Called during SignalMetadata.__post_init__() and at end of `_load_catalog()`.
514
+
515
+ **Benefits:**
516
+ - Enforces new architecture (no embedded computation in signals)
517
+ - Prevents mixing of old and new patterns
518
+ - Clear migration path for deprecated fields
519
+
520
+ ### StrategyRegistry Validation
521
+
522
+ ```python
523
+ @dataclass(frozen=True)
524
+ class StrategyMetadata:
525
+ # ... fields ...
526
+
527
+ def __post_init__(self) -> None:
528
+ """Validate strategy metadata."""
529
+ if self.position_size_mm <= 0:
530
+ raise ValueError(
531
+ f"Strategy '{self.name}': position_size_mm must be positive, "
532
+ f"got {self.position_size_mm}"
533
+ )
534
+ if self.sizing_mode not in {"binary", "proportional"}:
535
+ raise ValueError(
536
+ f"Strategy '{self.name}': sizing_mode must be 'binary' or 'proportional', "
537
+ f"got '{self.sizing_mode}'"
538
+ )
539
+ ```
540
+
541
+ **Timing:** Runs during dataclass instantiation (in `_load_catalog()`).
542
+
543
+ **Benefits:**
544
+ - Enforces valid position sizing parameters
545
+ - Prevents invalid BacktestConfig creation
546
+ - No need for separate validation step
547
+
548
+ ---
549
+
550
+ **End of Document**
551
+