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,304 @@
1
+ """
2
+ Return attribution and decomposition analysis.
3
+
4
+ Provides tools for attributing backtest returns to various sources including
5
+ trade direction, signal strength, and win/loss patterns.
6
+ """
7
+
8
+ import logging
9
+
10
+ import pandas as pd
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def attribute_by_direction(
16
+ pnl_df: pd.DataFrame,
17
+ positions_df: pd.DataFrame,
18
+ ) -> dict[str, float]:
19
+ """
20
+ Attribute returns by trade direction (long vs short).
21
+
22
+ Parameters
23
+ ----------
24
+ pnl_df : pd.DataFrame
25
+ P&L DataFrame with 'net_pnl' column.
26
+ positions_df : pd.DataFrame
27
+ Position DataFrame with 'position' column (+1 for long, -1 for short).
28
+
29
+ Returns
30
+ -------
31
+ dict[str, float]
32
+ Attribution with keys:
33
+ - 'long_pnl': Total P&L from long positions
34
+ - 'short_pnl': Total P&L from short positions
35
+ - 'long_pct': Percentage of total P&L from longs
36
+ - 'short_pct': Percentage of total P&L from shorts
37
+
38
+ Notes
39
+ -----
40
+ For CDX overlay strategies:
41
+ - Long position = sell protection (bullish credit)
42
+ - Short position = buy protection (bearish credit)
43
+
44
+ Examples
45
+ --------
46
+ >>> direction_attr = attribute_by_direction(result.pnl, result.positions)
47
+ >>> print(f"Long contributed: {direction_attr['long_pct']:.1%}")
48
+ """
49
+ logger.debug("Computing directional attribution")
50
+
51
+ # Align indices
52
+ aligned_pnl = pnl_df.reindex(positions_df.index)["net_pnl"]
53
+ position = positions_df["position"]
54
+
55
+ # Separate P&L by direction
56
+ long_mask = position > 0
57
+ short_mask = position < 0
58
+
59
+ long_pnl = aligned_pnl[long_mask].sum()
60
+ short_pnl = aligned_pnl[short_mask].sum()
61
+ total_pnl = long_pnl + short_pnl
62
+
63
+ # Compute percentages
64
+ if abs(total_pnl) > 0:
65
+ long_pct = long_pnl / total_pnl
66
+ short_pct = short_pnl / total_pnl
67
+ else:
68
+ long_pct = 0.0
69
+ short_pct = 0.0
70
+
71
+ logger.debug(
72
+ "Direction attribution: long=%.2f (%.1f%%), short=%.2f (%.1f%%)",
73
+ long_pnl,
74
+ long_pct * 100,
75
+ short_pnl,
76
+ short_pct * 100,
77
+ )
78
+
79
+ return {
80
+ "long_pnl": long_pnl,
81
+ "short_pnl": short_pnl,
82
+ "long_pct": long_pct,
83
+ "short_pct": short_pct,
84
+ }
85
+
86
+
87
+ def attribute_by_signal_strength(
88
+ pnl_df: pd.DataFrame,
89
+ positions_df: pd.DataFrame,
90
+ n_quantiles: int = 3,
91
+ ) -> dict[str, float]:
92
+ """
93
+ Attribute returns by signal strength quantiles.
94
+
95
+ Separates P&L based on absolute signal strength at position entry,
96
+ using quantile buckets (e.g., weak/medium/strong signals).
97
+
98
+ Parameters
99
+ ----------
100
+ pnl_df : pd.DataFrame
101
+ P&L DataFrame with 'net_pnl' column.
102
+ positions_df : pd.DataFrame
103
+ Position DataFrame with 'signal' and 'position' columns.
104
+ n_quantiles : int
105
+ Number of quantile buckets. Default: 3 (terciles).
106
+
107
+ Returns
108
+ -------
109
+ dict[str, float]
110
+ Attribution by quantile with keys:
111
+ - 'q1_pnl', 'q2_pnl', ...: P&L per quantile
112
+ - 'q1_pct', 'q2_pct', ...: Percentage contribution per quantile
113
+ - 'quantile_labels': List of quantile labels
114
+
115
+ Notes
116
+ -----
117
+ Uses absolute signal values to handle both long and short positions.
118
+ Quantiles are computed on days when positioned (position != 0).
119
+
120
+ Examples
121
+ --------
122
+ >>> signal_attr = attribute_by_signal_strength(result.pnl, result.positions, n_quantiles=3)
123
+ >>> print(f"Strongest signals: {signal_attr['q3_pct']:.1%}")
124
+ """
125
+ logger.debug("Computing signal strength attribution: n_quantiles=%d", n_quantiles)
126
+
127
+ # Filter to positioned days only
128
+ positioned = positions_df[positions_df["position"] != 0].copy()
129
+
130
+ if len(positioned) == 0:
131
+ logger.warning("No positioned days found for signal attribution")
132
+ return (
133
+ {f"q{i + 1}_pnl": 0.0 for i in range(n_quantiles)}
134
+ | {f"q{i + 1}_pct": 0.0 for i in range(n_quantiles)}
135
+ | {"quantile_labels": [f"Q{i + 1}" for i in range(n_quantiles)]}
136
+ )
137
+
138
+ # Use absolute signal strength
139
+ positioned["abs_signal"] = positioned["signal"].abs()
140
+
141
+ # Assign quantiles (1 = weakest, n_quantiles = strongest)
142
+ positioned["quantile"] = pd.qcut(
143
+ positioned["abs_signal"],
144
+ q=n_quantiles,
145
+ labels=range(1, n_quantiles + 1),
146
+ duplicates="drop",
147
+ )
148
+
149
+ # Align P&L
150
+ aligned_pnl = pnl_df.reindex(positioned.index)["net_pnl"]
151
+
152
+ # Aggregate by quantile
153
+ quantile_pnl = aligned_pnl.groupby(positioned["quantile"], observed=True).sum()
154
+ total_pnl = aligned_pnl.sum()
155
+
156
+ # Build result dictionary
157
+ result = {}
158
+ for q in range(1, n_quantiles + 1):
159
+ pnl_value = quantile_pnl.get(q, 0.0)
160
+ pct_value = pnl_value / total_pnl if abs(total_pnl) > 0 else 0.0
161
+
162
+ result[f"q{q}_pnl"] = pnl_value
163
+ result[f"q{q}_pct"] = pct_value
164
+
165
+ result["quantile_labels"] = [f"Q{i + 1}" for i in range(n_quantiles)]
166
+
167
+ logger.debug(
168
+ "Signal strength attribution: %s",
169
+ ", ".join(
170
+ [f"Q{i + 1}={result[f'q{i + 1}_pct']:.1%}" for i in range(n_quantiles)]
171
+ ),
172
+ )
173
+
174
+ return result
175
+
176
+
177
+ def attribute_by_win_loss(
178
+ pnl_df: pd.DataFrame,
179
+ positions_df: pd.DataFrame,
180
+ ) -> dict[str, float]:
181
+ """
182
+ Decompose P&L into winning and losing trade contributions.
183
+
184
+ Parameters
185
+ ----------
186
+ pnl_df : pd.DataFrame
187
+ P&L DataFrame with 'net_pnl' column.
188
+ positions_df : pd.DataFrame
189
+ Position DataFrame with 'position' column.
190
+
191
+ Returns
192
+ -------
193
+ dict[str, float]
194
+ Win/loss attribution with keys:
195
+ - 'gross_wins': Sum of all positive daily P&L
196
+ - 'gross_losses': Sum of all negative daily P&L (negative value)
197
+ - 'net_pnl': gross_wins + gross_losses
198
+ - 'win_contribution': Percentage from wins
199
+ - 'loss_contribution': Percentage from losses
200
+
201
+ Notes
202
+ -----
203
+ This is a daily P&L decomposition, not trade-level.
204
+ Useful for understanding contribution from up-days vs down-days.
205
+
206
+ Examples
207
+ --------
208
+ >>> wl_attr = attribute_by_win_loss(result.pnl, result.positions)
209
+ >>> print(f"Wins contributed: {wl_attr['win_contribution']:.1%}")
210
+ """
211
+ logger.debug("Computing win/loss attribution")
212
+
213
+ # Only include positioned days
214
+ positioned_mask = positions_df["position"] != 0
215
+ aligned_pnl = pnl_df.reindex(positions_df.index).loc[positioned_mask, "net_pnl"]
216
+
217
+ # Separate wins and losses
218
+ gross_wins = aligned_pnl[aligned_pnl > 0].sum()
219
+ gross_losses = aligned_pnl[aligned_pnl < 0].sum() # Negative value
220
+ net_pnl = gross_wins + gross_losses
221
+
222
+ # Compute contributions
223
+ if abs(net_pnl) > 0:
224
+ win_contribution = gross_wins / abs(net_pnl)
225
+ loss_contribution = abs(gross_losses) / abs(net_pnl)
226
+ else:
227
+ win_contribution = 0.0
228
+ loss_contribution = 0.0
229
+
230
+ logger.debug(
231
+ "Win/loss: wins=%.2f (%.1f%%), losses=%.2f (%.1f%%)",
232
+ gross_wins,
233
+ win_contribution * 100,
234
+ gross_losses,
235
+ loss_contribution * 100,
236
+ )
237
+
238
+ return {
239
+ "gross_wins": gross_wins,
240
+ "gross_losses": gross_losses,
241
+ "net_pnl": net_pnl,
242
+ "win_contribution": win_contribution,
243
+ "loss_contribution": loss_contribution,
244
+ }
245
+
246
+
247
+ def compute_attribution(
248
+ pnl_df: pd.DataFrame,
249
+ positions_df: pd.DataFrame,
250
+ n_quantiles: int = 3,
251
+ ) -> dict[str, dict[str, float]]:
252
+ """
253
+ Compute all attribution analyses.
254
+
255
+ Orchestrates computation of directional, signal strength, and
256
+ win/loss attribution.
257
+
258
+ Parameters
259
+ ----------
260
+ pnl_df : pd.DataFrame
261
+ P&L DataFrame with 'net_pnl' column.
262
+ positions_df : pd.DataFrame
263
+ Position DataFrame with 'signal' and 'position' columns.
264
+ n_quantiles : int
265
+ Number of quantiles for signal strength attribution. Default: 3.
266
+
267
+ Returns
268
+ -------
269
+ dict[str, dict[str, float]]
270
+ Nested dictionary with keys:
271
+ - 'direction': Directional attribution
272
+ - 'signal_strength': Signal quantile attribution
273
+ - 'win_loss': Win/loss decomposition
274
+
275
+ Notes
276
+ -----
277
+ This function provides comprehensive return attribution for
278
+ understanding sources of backtest performance.
279
+
280
+ Examples
281
+ --------
282
+ >>> attribution = compute_attribution(result.pnl, result.positions, n_quantiles=3)
283
+ >>> print(f"Long P&L: ${attribution['direction']['long_pnl']:,.0f}")
284
+ >>> print(f"Strongest signals: {attribution['signal_strength']['q3_pct']:.1%}")
285
+ """
286
+ logger.info("Computing return attribution: n_quantiles=%d", n_quantiles)
287
+
288
+ direction_attr = attribute_by_direction(pnl_df, positions_df)
289
+ signal_attr = attribute_by_signal_strength(pnl_df, positions_df, n_quantiles)
290
+ wl_attr = attribute_by_win_loss(pnl_df, positions_df)
291
+
292
+ attribution = {
293
+ "direction": direction_attr,
294
+ "signal_strength": signal_attr,
295
+ "win_loss": wl_attr,
296
+ }
297
+
298
+ logger.info(
299
+ "Attribution computed: long=%.1f%%, wins=%.1f%%",
300
+ direction_attr["long_pct"] * 100,
301
+ wl_attr["win_contribution"] * 100,
302
+ )
303
+
304
+ return attribution