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,334 @@
1
+ """
2
+ Strategy registry for managing backtest strategy metadata and catalog persistence.
3
+
4
+ Follows the same governance pattern as SignalRegistry for consistency.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from dataclasses import dataclass, asdict
10
+ from pathlib import Path
11
+
12
+ from .config import BacktestConfig
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class StrategyMetadata:
19
+ """
20
+ Metadata for a registered backtest strategy.
21
+
22
+ Attributes
23
+ ----------
24
+ name : str
25
+ Unique strategy identifier (e.g., "conservative", "balanced").
26
+ description : str
27
+ Human-readable description of strategy characteristics.
28
+ position_size_mm : float
29
+ Baseline notional position size in millions.
30
+ sizing_mode : str
31
+ Position sizing mode: 'binary' (full position for any non-zero signal)
32
+ or 'proportional' (scaled by signal magnitude).
33
+ stop_loss_pct : float | None
34
+ Stop loss as percentage of initial position value. None to disable.
35
+ take_profit_pct : float | None
36
+ Take profit as percentage of initial position value. None to disable.
37
+ max_holding_days : int | None
38
+ Maximum days to hold a position before forced exit. None for no limit.
39
+ transaction_cost_bps : float
40
+ Round-trip transaction cost in basis points.
41
+ dv01_per_million : float
42
+ DV01 per $1MM notional for risk calculations.
43
+ enabled : bool
44
+ Whether strategy should be included in evaluation.
45
+ """
46
+
47
+ name: str
48
+ description: str
49
+ position_size_mm: float
50
+ sizing_mode: str
51
+ stop_loss_pct: float | None
52
+ take_profit_pct: float | None
53
+ max_holding_days: int | None
54
+ transaction_cost_bps: float
55
+ dv01_per_million: float
56
+ enabled: bool = True
57
+
58
+ def __post_init__(self) -> None:
59
+ """Validate strategy metadata."""
60
+ if not self.name:
61
+ raise ValueError("Strategy name cannot be empty")
62
+ if self.position_size_mm <= 0:
63
+ raise ValueError(
64
+ f"Strategy '{self.name}': position_size_mm must be positive, "
65
+ f"got {self.position_size_mm}"
66
+ )
67
+ if self.sizing_mode not in {"binary", "proportional"}:
68
+ raise ValueError(
69
+ f"Strategy '{self.name}': sizing_mode must be 'binary' or 'proportional', "
70
+ f"got '{self.sizing_mode}'"
71
+ )
72
+ if self.stop_loss_pct is not None and not (0 < self.stop_loss_pct <= 100):
73
+ raise ValueError(
74
+ f"Strategy '{self.name}': stop_loss_pct must be in (0, 100], "
75
+ f"got {self.stop_loss_pct}"
76
+ )
77
+ if self.take_profit_pct is not None and not (0 < self.take_profit_pct <= 100):
78
+ raise ValueError(
79
+ f"Strategy '{self.name}': take_profit_pct must be in (0, 100], "
80
+ f"got {self.take_profit_pct}"
81
+ )
82
+ if self.max_holding_days is not None and self.max_holding_days <= 0:
83
+ raise ValueError(
84
+ f"Strategy '{self.name}': max_holding_days must be positive, "
85
+ f"got {self.max_holding_days}"
86
+ )
87
+ if self.transaction_cost_bps < 0:
88
+ raise ValueError(
89
+ f"Strategy '{self.name}': transaction_cost_bps must be non-negative, "
90
+ f"got {self.transaction_cost_bps}"
91
+ )
92
+ if self.dv01_per_million <= 0:
93
+ raise ValueError(
94
+ f"Strategy '{self.name}': dv01_per_million must be positive, "
95
+ f"got {self.dv01_per_million}"
96
+ )
97
+
98
+ def to_config(
99
+ self,
100
+ position_size_mm_override: float | None = None,
101
+ sizing_mode_override: str | None = None,
102
+ stop_loss_pct_override: float | None = None,
103
+ take_profit_pct_override: float | None = None,
104
+ max_holding_days_override: int | None = None,
105
+ ) -> BacktestConfig:
106
+ """
107
+ Convert strategy metadata to BacktestConfig.
108
+
109
+ Supports runtime parameter overrides for rapid experimentation.
110
+
111
+ Parameters
112
+ ----------
113
+ position_size_mm_override : float | None, default None
114
+ Override catalog position_size_mm value.
115
+ sizing_mode_override : str | None, default None
116
+ Override catalog sizing_mode value.
117
+ stop_loss_pct_override : float | None, default None
118
+ Override catalog stop_loss_pct value (use False to explicitly disable).
119
+ take_profit_pct_override : float | None, default None
120
+ Override catalog take_profit_pct value (use False to explicitly disable).
121
+ max_holding_days_override : int | None, default None
122
+ Override catalog max_holding_days value (use False to explicitly disable).
123
+
124
+ Returns
125
+ -------
126
+ BacktestConfig
127
+ Full backtest configuration with strategy parameters.
128
+
129
+ Examples
130
+ --------
131
+ >>> metadata = StrategyMetadata(
132
+ ... name="aggressive",
133
+ ... description="High risk tolerance",
134
+ ... position_size_mm=15.0,
135
+ ... sizing_mode="binary"
136
+ ... )
137
+ >>> config = metadata.to_config(position_size_mm_override=20.0)
138
+ >>> config.position_size_mm
139
+ 20.0
140
+ """
141
+ return BacktestConfig(
142
+ position_size_mm=(
143
+ position_size_mm_override
144
+ if position_size_mm_override is not None
145
+ else self.position_size_mm
146
+ ),
147
+ sizing_mode=(
148
+ sizing_mode_override
149
+ if sizing_mode_override is not None
150
+ else self.sizing_mode
151
+ ),
152
+ stop_loss_pct=(
153
+ stop_loss_pct_override
154
+ if stop_loss_pct_override is not None
155
+ else self.stop_loss_pct
156
+ ),
157
+ take_profit_pct=(
158
+ take_profit_pct_override
159
+ if take_profit_pct_override is not None
160
+ else self.take_profit_pct
161
+ ),
162
+ max_holding_days=(
163
+ max_holding_days_override
164
+ if max_holding_days_override is not None
165
+ else self.max_holding_days
166
+ ),
167
+ transaction_cost_bps=self.transaction_cost_bps,
168
+ dv01_per_million=self.dv01_per_million,
169
+ )
170
+
171
+
172
+ class StrategyRegistry:
173
+ """
174
+ Registry for strategy metadata with JSON catalog persistence.
175
+
176
+ Manages strategy definitions, enabling/disabling strategies, and catalog I/O.
177
+ Follows pattern from models.registry.SignalRegistry.
178
+
179
+ Parameters
180
+ ----------
181
+ catalog_path : str | Path
182
+ Path to JSON catalog file containing strategy metadata.
183
+
184
+ Examples
185
+ --------
186
+ >>> from aponyx.config import STRATEGY_CATALOG_PATH
187
+ >>> registry = StrategyRegistry(STRATEGY_CATALOG_PATH)
188
+ >>> enabled = registry.get_enabled()
189
+ >>> metadata = registry.get_metadata("balanced")
190
+ >>> config = metadata.to_config()
191
+ """
192
+
193
+ def __init__(self, catalog_path: str | Path) -> None:
194
+ """
195
+ Initialize registry and load catalog from JSON file.
196
+
197
+ Parameters
198
+ ----------
199
+ catalog_path : str | Path
200
+ Path to JSON catalog file.
201
+
202
+ Raises
203
+ ------
204
+ FileNotFoundError
205
+ If catalog file does not exist.
206
+ ValueError
207
+ If catalog JSON is invalid or contains duplicate strategy names.
208
+ """
209
+ self._catalog_path = Path(catalog_path)
210
+ self._strategies: dict[str, StrategyMetadata] = {}
211
+ self._load_catalog()
212
+
213
+ logger.info(
214
+ "Loaded strategy registry: catalog=%s, strategies=%d, enabled=%d",
215
+ self._catalog_path,
216
+ len(self._strategies),
217
+ len(self.get_enabled()),
218
+ )
219
+
220
+ def _load_catalog(self) -> None:
221
+ """Load strategy metadata from JSON catalog file."""
222
+ if not self._catalog_path.exists():
223
+ raise FileNotFoundError(f"Strategy catalog not found: {self._catalog_path}")
224
+
225
+ with open(self._catalog_path, "r", encoding="utf-8") as f:
226
+ catalog_data = json.load(f)
227
+
228
+ if not isinstance(catalog_data, list):
229
+ raise ValueError("Strategy catalog must be a JSON array")
230
+
231
+ for entry in catalog_data:
232
+ try:
233
+ metadata = StrategyMetadata(**entry)
234
+ if metadata.name in self._strategies:
235
+ raise ValueError(
236
+ f"Duplicate strategy name in catalog: {metadata.name}"
237
+ )
238
+ self._strategies[metadata.name] = metadata
239
+ except TypeError as e:
240
+ raise ValueError(
241
+ f"Invalid strategy metadata in catalog: {entry}. Error: {e}"
242
+ ) from e
243
+
244
+ logger.debug("Loaded %d strategies from catalog", len(self._strategies))
245
+
246
+ # Fail-fast validation: thresholds already validated in __post_init__
247
+ # No additional validation needed beyond dataclass constraints
248
+
249
+ def get_metadata(self, name: str) -> StrategyMetadata:
250
+ """
251
+ Retrieve metadata for a specific strategy.
252
+
253
+ Parameters
254
+ ----------
255
+ name : str
256
+ Strategy name.
257
+
258
+ Returns
259
+ -------
260
+ StrategyMetadata
261
+ Strategy metadata.
262
+
263
+ Raises
264
+ ------
265
+ KeyError
266
+ If strategy name is not registered.
267
+ """
268
+ if name not in self._strategies:
269
+ raise KeyError(
270
+ f"Strategy '{name}' not found in registry. "
271
+ f"Available strategies: {sorted(self._strategies.keys())}"
272
+ )
273
+ return self._strategies[name]
274
+
275
+ def get_enabled(self) -> dict[str, StrategyMetadata]:
276
+ """
277
+ Get all enabled strategies.
278
+
279
+ Returns
280
+ -------
281
+ dict[str, StrategyMetadata]
282
+ Mapping from strategy name to metadata for enabled strategies only.
283
+ """
284
+ return {name: meta for name, meta in self._strategies.items() if meta.enabled}
285
+
286
+ def list_all(self) -> dict[str, StrategyMetadata]:
287
+ """
288
+ Get all registered strategies (enabled and disabled).
289
+
290
+ Returns
291
+ -------
292
+ dict[str, StrategyMetadata]
293
+ Mapping from strategy name to metadata for all strategies.
294
+ """
295
+ return self._strategies.copy()
296
+
297
+ def strategy_exists(self, name: str) -> bool:
298
+ """
299
+ Check if strategy is registered.
300
+
301
+ Parameters
302
+ ----------
303
+ name : str
304
+ Strategy name.
305
+
306
+ Returns
307
+ -------
308
+ bool
309
+ True if strategy exists in registry.
310
+ """
311
+ return name in self._strategies
312
+
313
+ def save_catalog(self, path: str | Path | None = None) -> None:
314
+ """
315
+ Save strategy metadata to JSON catalog file.
316
+
317
+ Parameters
318
+ ----------
319
+ path : str | Path | None
320
+ Output path. If None, overwrites original catalog file.
321
+ """
322
+ output_path = Path(path) if path else self._catalog_path
323
+
324
+ catalog_data = [asdict(meta) for meta in self._strategies.values()]
325
+
326
+ output_path.parent.mkdir(parents=True, exist_ok=True)
327
+ with open(output_path, "w", encoding="utf-8") as f:
328
+ json.dump(catalog_data, f, indent=2)
329
+
330
+ logger.info(
331
+ "Saved strategy catalog: path=%s, strategies=%d",
332
+ output_path,
333
+ len(catalog_data),
334
+ )
@@ -0,0 +1,50 @@
1
+ [
2
+ {
3
+ "name": "conservative",
4
+ "description": "Conservative sizing with tight risk management for low-turnover trades",
5
+ "position_size_mm": 5.0,
6
+ "sizing_mode": "proportional",
7
+ "stop_loss_pct": 3.0,
8
+ "take_profit_pct": 8.0,
9
+ "max_holding_days": 20,
10
+ "transaction_cost_bps": 1.0,
11
+ "dv01_per_million": 475.0,
12
+ "enabled": true
13
+ },
14
+ {
15
+ "name": "balanced",
16
+ "description": "Balanced position sizing with moderate risk management",
17
+ "position_size_mm": 10.0,
18
+ "sizing_mode": "proportional",
19
+ "stop_loss_pct": 5.0,
20
+ "take_profit_pct": 10.0,
21
+ "max_holding_days": null,
22
+ "transaction_cost_bps": 1.0,
23
+ "dv01_per_million": 475.0,
24
+ "enabled": true
25
+ },
26
+ {
27
+ "name": "aggressive",
28
+ "description": "Aggressive sizing with wide risk bands for high-turnover trading",
29
+ "position_size_mm": 15.0,
30
+ "sizing_mode": "proportional",
31
+ "stop_loss_pct": 10.0,
32
+ "take_profit_pct": null,
33
+ "max_holding_days": null,
34
+ "transaction_cost_bps": 1.0,
35
+ "dv01_per_million": 475.0,
36
+ "enabled": true
37
+ },
38
+ {
39
+ "name": "experimental",
40
+ "description": "Experimental configuration for research and testing",
41
+ "position_size_mm": 10.0,
42
+ "sizing_mode": "proportional",
43
+ "stop_loss_pct": null,
44
+ "take_profit_pct": null,
45
+ "max_holding_days": null,
46
+ "transaction_cost_bps": 1.0,
47
+ "dv01_per_million": 475.0,
48
+ "enabled": false
49
+ }
50
+ ]
aponyx/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Command-line interface for systematic macro credit research."""
2
+
3
+ from .main import cli
4
+
5
+ __all__ = ["cli"]
@@ -0,0 +1,8 @@
1
+ """CLI command implementations."""
2
+
3
+ from .run import run
4
+ from .report import report
5
+ from .list import list_items
6
+ from .clean import clean
7
+
8
+ __all__ = ["run", "report", "list_items", "clean"]