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,383 @@
1
+ """Bloomberg Terminal/API data provider.
2
+
3
+ Fetches market data using Bloomberg's Python API via xbbg wrapper.
4
+ Requires active Bloomberg Terminal session.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime, timedelta
9
+ from typing import Any
10
+ from zoneinfo import ZoneInfo
11
+
12
+ import pandas as pd
13
+
14
+ from ..bloomberg_config import (
15
+ BloombergInstrumentSpec,
16
+ get_instrument_spec,
17
+ get_security_from_ticker,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def fetch_from_bloomberg(
24
+ ticker: str,
25
+ instrument: str,
26
+ start_date: str | None = None,
27
+ end_date: str | None = None,
28
+ security: str | None = None,
29
+ **params: Any,
30
+ ) -> pd.DataFrame:
31
+ """
32
+ Fetch historical data from Bloomberg Terminal via xbbg wrapper.
33
+
34
+ Parameters
35
+ ----------
36
+ ticker : str
37
+ Bloomberg ticker (e.g., 'CDX IG CDSI GEN 5Y Corp', 'VIX Index', 'HYG US Equity').
38
+ instrument : str
39
+ Instrument type for field mapping ('cdx', 'vix', 'etf').
40
+ start_date : str or None, default None
41
+ Start date in YYYY-MM-DD format. Defaults to 5 years ago.
42
+ end_date : str or None, default None
43
+ End date in YYYY-MM-DD format. Defaults to today.
44
+ security : str or None, default None
45
+ Internal security identifier (e.g., 'cdx_ig_5y', 'hyg').
46
+ If provided, used directly for metadata. Otherwise, reverse lookup from ticker.
47
+ **params : Any
48
+ Additional Bloomberg request parameters passed to xbbg.
49
+
50
+ Returns
51
+ -------
52
+ pd.DataFrame
53
+ Historical data with DatetimeIndex and schema-compatible columns.
54
+
55
+ Raises
56
+ ------
57
+ ImportError
58
+ If xbbg is not installed.
59
+ ValueError
60
+ If ticker format is invalid or instrument type is unknown.
61
+ RuntimeError
62
+ If Bloomberg request fails or returns empty data.
63
+
64
+ Notes
65
+ -----
66
+ Requires active Bloomberg Terminal session. Connection is handled
67
+ automatically by xbbg wrapper.
68
+
69
+ Returned DataFrame columns are mapped to project schemas:
70
+ - CDX: spread, security
71
+ - VIX: level
72
+ - ETF: spread, security
73
+
74
+ Example tickers:
75
+ - CDX: 'CDX IG CDSI GEN 5Y Corp'
76
+ - VIX: 'VIX Index'
77
+ - ETFs: 'HYG US Equity', 'LQD US Equity'
78
+ """
79
+ # Get instrument specification from registry
80
+ spec = get_instrument_spec(instrument)
81
+
82
+ # Default to 5-year lookback if dates not provided
83
+ if end_date is None:
84
+ end_date = datetime.now().strftime("%Y-%m-%d")
85
+ if start_date is None:
86
+ start_dt = datetime.now() - timedelta(days=5 * 365)
87
+ start_date = start_dt.strftime("%Y-%m-%d")
88
+
89
+ # Convert dates to Bloomberg format (YYYYMMDD)
90
+ bbg_start = start_date.replace("-", "")
91
+ bbg_end = end_date.replace("-", "")
92
+
93
+ logger.info(
94
+ "Fetching %s from Bloomberg: ticker=%s, dates=%s to %s",
95
+ instrument,
96
+ ticker,
97
+ start_date,
98
+ end_date,
99
+ )
100
+
101
+ # Import xbbg wrapper
102
+ # Note: Use BaseException to catch pytest.Skipped from xbbg's importorskip
103
+ try:
104
+ from xbbg import blp
105
+ except BaseException as e:
106
+ # Handle multiple error types:
107
+ # 1. Direct ImportError when xbbg not installed
108
+ # 2. ImportError with blpapi in message (nested import failure)
109
+ # 3. pytest.Skipped exception from xbbg's importorskip
110
+ error_msg = str(e)
111
+
112
+ if "blpapi" in error_msg.lower():
113
+ raise ImportError(
114
+ "Bloomberg API (blpapi) not installed. "
115
+ "Install with: uv pip install blpapi\n"
116
+ "Or install all Bloomberg dependencies: uv sync --extra bloomberg\n"
117
+ "Note: Requires active Bloomberg Terminal subscription."
118
+ ) from e
119
+ elif isinstance(e, ImportError):
120
+ raise ImportError(
121
+ f"xbbg not installed: {error_msg}\n"
122
+ "Install with: uv pip install xbbg\n"
123
+ "Or install all Bloomberg dependencies: uv sync --extra bloomberg"
124
+ ) from e
125
+ else:
126
+ # Re-raise other exceptions (KeyboardInterrupt, SystemExit, etc.)
127
+ raise
128
+
129
+ # Fetch historical data using xbbg
130
+ try:
131
+ df = blp.bdh(
132
+ tickers=ticker,
133
+ flds=spec.bloomberg_fields,
134
+ start_date=bbg_start,
135
+ end_date=bbg_end,
136
+ **params,
137
+ )
138
+ except Exception as e:
139
+ logger.error("Bloomberg request failed: %s", str(e))
140
+ raise RuntimeError(f"Failed to fetch data from Bloomberg: {e}") from e
141
+
142
+ # Check if response is empty
143
+ if df is None or df.empty:
144
+ raise RuntimeError(
145
+ f"Bloomberg returned empty data for {ticker}. "
146
+ "Check ticker format and data availability."
147
+ )
148
+
149
+ logger.debug("Fetched %d rows from Bloomberg", len(df))
150
+
151
+ # Convert index to DatetimeIndex (xbbg returns object dtype)
152
+ df.index = pd.to_datetime(df.index)
153
+ logger.debug("Converted index to DatetimeIndex: %s", df.index.dtype)
154
+
155
+ # Map Bloomberg field names to schema columns
156
+ df = _map_bloomberg_fields(df, spec)
157
+
158
+ # Add metadata columns (security identifier)
159
+ if spec.requires_security_metadata:
160
+ df = _add_security_metadata(df, ticker, security)
161
+
162
+ logger.info(
163
+ "Successfully fetched %d rows with columns: %s", len(df), list(df.columns)
164
+ )
165
+
166
+ return df
167
+
168
+
169
+ def _map_bloomberg_fields(
170
+ df: pd.DataFrame,
171
+ spec: BloombergInstrumentSpec,
172
+ ) -> pd.DataFrame:
173
+ """
174
+ Map Bloomberg field names to schema-expected column names.
175
+
176
+ Parameters
177
+ ----------
178
+ df : pd.DataFrame
179
+ Raw DataFrame from xbbg with Bloomberg field names.
180
+ spec : BloombergInstrumentSpec
181
+ Instrument specification with field mappings.
182
+
183
+ Returns
184
+ -------
185
+ pd.DataFrame
186
+ DataFrame with renamed columns matching project schemas.
187
+
188
+ Notes
189
+ -----
190
+ BDH returns multi-index columns: (ticker, field) with uppercase fields.
191
+ BDP returns flat columns: fields with lowercase.
192
+ We normalize to uppercase before mapping.
193
+ """
194
+ # Handle xbbg multi-index columns: (ticker, field) from BDH
195
+ if isinstance(df.columns, pd.MultiIndex):
196
+ # Flatten by taking second level (field names)
197
+ df.columns = df.columns.get_level_values(1)
198
+ logger.debug("Flattened multi-index columns from BDH")
199
+ else:
200
+ # BDP returns flat columns (single ticker)
201
+ # Normalize lowercase fields to uppercase for consistent mapping
202
+ df.columns = df.columns.str.upper()
203
+ logger.debug("Normalized BDP field names to uppercase")
204
+
205
+ # Rename columns according to mapping
206
+ df = df.rename(columns=spec.field_mapping)
207
+
208
+ logger.debug(
209
+ "Mapped fields: %s -> %s",
210
+ list(spec.field_mapping.keys()),
211
+ list(spec.field_mapping.values()),
212
+ )
213
+
214
+ return df
215
+
216
+
217
+ def _add_security_metadata(
218
+ df: pd.DataFrame,
219
+ ticker: str,
220
+ security: str | None = None,
221
+ ) -> pd.DataFrame:
222
+ """
223
+ Add security metadata column.
224
+
225
+ Parameters
226
+ ----------
227
+ df : pd.DataFrame
228
+ DataFrame with mapped field columns.
229
+ ticker : str
230
+ Bloomberg ticker string.
231
+ security : str or None
232
+ Internal security identifier. If None, reverse lookup from ticker.
233
+
234
+ Returns
235
+ -------
236
+ pd.DataFrame
237
+ DataFrame with added 'security' column.
238
+
239
+ Raises
240
+ ------
241
+ ValueError
242
+ If security not provided and ticker not found in registry.
243
+ """
244
+ # Get security identifier from parameter or reverse lookup
245
+ if security is not None:
246
+ sec_id = security
247
+ logger.debug("Using provided security identifier: %s", sec_id)
248
+ else:
249
+ # Reverse lookup from Bloomberg ticker
250
+ try:
251
+ sec_id = get_security_from_ticker(ticker)
252
+ logger.debug("Reverse lookup: %s -> %s", ticker, sec_id)
253
+ except ValueError as e:
254
+ logger.error("Failed to resolve security from ticker: %s", ticker)
255
+ raise ValueError(
256
+ f"Cannot determine security identifier for ticker '{ticker}'. "
257
+ "Either provide 'security' parameter or ensure ticker is in registry."
258
+ ) from e
259
+
260
+ df["security"] = sec_id
261
+ logger.debug("Added security metadata: %s", sec_id)
262
+
263
+ return df
264
+
265
+
266
+ def fetch_current_from_bloomberg(
267
+ ticker: str,
268
+ instrument: str,
269
+ security: str | None = None,
270
+ **params: Any,
271
+ ) -> pd.DataFrame | None:
272
+ """
273
+ Fetch current/latest data point from Bloomberg using BDP.
274
+
275
+ Parameters
276
+ ----------
277
+ ticker : str
278
+ Bloomberg ticker.
279
+ instrument : str
280
+ Instrument type for field mapping ('cdx', 'vix', 'etf').
281
+ security : str or None
282
+ Internal security identifier.
283
+ **params : Any
284
+ Additional Bloomberg request parameters.
285
+
286
+ Returns
287
+ -------
288
+ pd.DataFrame or None
289
+ Single-row DataFrame with current data and today's date as index.
290
+ Returns None if no data available (e.g., non-trading day).
291
+
292
+ Raises
293
+ ------
294
+ ImportError
295
+ If xbbg is not installed.
296
+ RuntimeError
297
+ If Bloomberg request fails due to connection/authentication issues.
298
+
299
+ Notes
300
+ -----
301
+ Uses Bloomberg's BDP (current data) instead of BDH (historical data).
302
+ Returns data with today's date (US/Eastern timezone) as the index.
303
+ Gracefully returns None on weekends/holidays instead of raising errors.
304
+ """
305
+ spec = get_instrument_spec(instrument)
306
+
307
+ logger.info(
308
+ "Fetching current %s from Bloomberg: ticker=%s",
309
+ instrument,
310
+ ticker,
311
+ )
312
+
313
+ try:
314
+ from xbbg import blp
315
+ except BaseException as e:
316
+ # Handle multiple error types:
317
+ # 1. Direct ImportError when xbbg not installed
318
+ # 2. ImportError with blpapi in message (nested import failure)
319
+ # 3. pytest.Skipped exception from xbbg's importorskip
320
+ error_msg = str(e)
321
+
322
+ if "blpapi" in error_msg.lower():
323
+ raise ImportError(
324
+ "Bloomberg API (blpapi) not installed. "
325
+ "Install with: uv pip install blpapi\n"
326
+ "Or install all Bloomberg dependencies: uv sync --extra bloomberg\n"
327
+ "Note: Requires active Bloomberg Terminal subscription."
328
+ ) from e
329
+ elif isinstance(e, ImportError):
330
+ raise ImportError(
331
+ f"xbbg not installed: {error_msg}\n"
332
+ "Install with: uv pip install xbbg\n"
333
+ "Or install all Bloomberg dependencies: uv sync --extra bloomberg"
334
+ ) from e
335
+ else:
336
+ # Re-raise other exceptions (KeyboardInterrupt, SystemExit, etc.)
337
+ raise
338
+
339
+ try:
340
+ # Use BDP for current data point
341
+ current_data = blp.bdp(
342
+ tickers=ticker,
343
+ flds=spec.bloomberg_fields,
344
+ **params,
345
+ )
346
+ except Exception as e:
347
+ logger.error("Bloomberg BDP request failed: %s", str(e))
348
+ raise RuntimeError(f"Failed to fetch current data from Bloomberg: {e}") from e
349
+
350
+ if current_data is None or current_data.empty:
351
+ # Gracefully handle no data (weekends, holidays, market closed)
352
+ logger.warning(
353
+ "Bloomberg BDP returned empty data for %s (likely non-trading day)",
354
+ ticker,
355
+ )
356
+ return None
357
+
358
+ logger.debug("Fetched current data from Bloomberg: %s", current_data.shape)
359
+
360
+ # Convert BDP format to time series format
361
+ # BDP returns: index=tickers, columns=fields (lowercase)
362
+ # Need: index=dates, columns=fields (to match BDH format)
363
+ eastern = ZoneInfo("America/New_York")
364
+ today = datetime.now(eastern).strftime("%Y-%m-%d")
365
+
366
+ # Extract single ticker row and reassign index to today's date
367
+ df = current_data.iloc[[0]].copy() # Keep as DataFrame with single row
368
+ df.index = pd.to_datetime([today])
369
+ df.index.name = "date"
370
+
371
+ # Map Bloomberg field names to schema columns
372
+ df = _map_bloomberg_fields(df, spec)
373
+
374
+ # Add security metadata if required
375
+ if spec.requires_security_metadata:
376
+ df = _add_security_metadata(df, ticker, security)
377
+
378
+ logger.info(
379
+ "Successfully fetched current data with columns: %s",
380
+ list(df.columns),
381
+ )
382
+
383
+ return df
@@ -0,0 +1,111 @@
1
+ """
2
+ File-based data provider for Parquet and CSV files.
3
+
4
+ Handles local file loading with automatic format detection.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ import pandas as pd
11
+
12
+ from ...persistence.parquet_io import load_parquet
13
+ from ..sources import FileSource
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def fetch_from_file(
19
+ source: FileSource,
20
+ ticker: str,
21
+ instrument: str,
22
+ security: str,
23
+ start_date: str | None = None,
24
+ end_date: str | None = None,
25
+ **params: Any,
26
+ ) -> pd.DataFrame:
27
+ """
28
+ Fetch data from local Parquet or CSV file using security-based lookup.
29
+
30
+ Parameters
31
+ ----------
32
+ source : FileSource
33
+ File source configuration with base_dir and security_mapping.
34
+ ticker : str
35
+ Ticker identifier (unused for file provider, for signature compatibility).
36
+ instrument : str
37
+ Instrument type (cdx, vix, etf).
38
+ security : str
39
+ Security identifier to fetch (e.g., 'cdx_ig_5y', 'vix', 'hyg').
40
+ start_date : str or None
41
+ Optional start date filter (ISO format, unused for file provider).
42
+ end_date : str or None
43
+ Optional end date filter (ISO format, unused for file provider).
44
+ **params : Any
45
+ Additional parameters (unused for file provider).
46
+
47
+ Returns
48
+ -------
49
+ pd.DataFrame
50
+ Raw data loaded from file (validation happens in fetch layer).
51
+
52
+ Raises
53
+ ------
54
+ ValueError
55
+ If security not found in mapping or file format not supported.
56
+ FileNotFoundError
57
+ If file does not exist.
58
+
59
+ Notes
60
+ -----
61
+ - Uses security_mapping to resolve security ID to filename
62
+ - Automatically detects Parquet vs CSV from file extension
63
+ - Adds 'security' column if instrument requires it
64
+ - Date filtering not performed (files pre-filtered to match needs)
65
+ """
66
+ # Resolve security to filename
67
+ if security not in source.security_mapping:
68
+ available = ", ".join(sorted(source.security_mapping.keys()))
69
+ raise ValueError(
70
+ f"Security '{security}' not found in registry. Available: {available}"
71
+ )
72
+
73
+ filename = source.security_mapping[security]
74
+ file_path = source.base_dir / filename
75
+
76
+ logger.info(
77
+ "Fetching %s (security=%s) from file: %s", instrument, security, file_path
78
+ )
79
+
80
+ if not file_path.exists():
81
+ raise FileNotFoundError(f"Data file not found: {file_path}")
82
+
83
+ # Load based on file type
84
+ if file_path.suffix == ".parquet":
85
+ df = load_parquet(file_path)
86
+ elif file_path.suffix == ".csv":
87
+ df = pd.read_csv(file_path)
88
+ # Convert 'date' column to DatetimeIndex if present
89
+ if "date" in df.columns:
90
+ df["date"] = pd.to_datetime(df["date"])
91
+ df = df.set_index("date")
92
+ logger.debug("Converted 'date' column to DatetimeIndex")
93
+ else:
94
+ raise ValueError(f"Unsupported file format: {file_path.suffix}")
95
+
96
+ # Add security column if instrument requires it
97
+ from ..bloomberg_config import get_instrument_spec
98
+
99
+ try:
100
+ inst_spec = get_instrument_spec(instrument)
101
+ if inst_spec.requires_security_metadata and "security" not in df.columns:
102
+ df["security"] = security
103
+ logger.debug("Added security column: %s", security)
104
+ except ValueError:
105
+ # Unknown instrument type, skip metadata enrichment
106
+ logger.debug(
107
+ "Unknown instrument type '%s', skipping metadata enrichment", instrument
108
+ )
109
+
110
+ logger.info("Loaded %d rows from file", len(df))
111
+ return df