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,541 @@
1
+ """
2
+ Markdown report generation for performance evaluation results.
3
+
4
+ Generates human-readable reports with performance metrics, attribution,
5
+ stability analysis, and interpretation guidance.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ import matplotlib.pyplot as plt
13
+ import pandas as pd
14
+ import quantstats as qs
15
+
16
+ from .config import PerformanceResult
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def generate_quantstats_tearsheet(
22
+ returns: pd.Series,
23
+ benchmark: pd.Series | None,
24
+ output_path: Path,
25
+ title: str,
26
+ periods_per_year: int = 252,
27
+ ) -> Path | None:
28
+ """
29
+ Generate quantstats HTML tearsheet from returns series.
30
+
31
+ Parameters
32
+ ----------
33
+ returns : pd.Series
34
+ Daily percentage returns with DatetimeIndex.
35
+ benchmark : pd.Series | None
36
+ Benchmark daily percentage returns with DatetimeIndex.
37
+ If None, generates tearsheet without benchmark comparison.
38
+ output_path : Path
39
+ Full path (including filename) for HTML tearsheet.
40
+ title : str
41
+ Title for the tearsheet report.
42
+ periods_per_year : int, default=252
43
+ Number of trading periods per year (252 for daily).
44
+
45
+ Returns
46
+ -------
47
+ Path | None
48
+ Path to saved tearsheet HTML file.
49
+ Returns None if quantstats unavailable or generation fails.
50
+
51
+ Notes
52
+ -----
53
+ - Requires quantstats library (install with viz extras: pip install -e ".[viz]")
54
+ - Fallback behavior: logs warning at DEBUG level and returns None
55
+ - Uses quantstats default tearsheet template with all sections
56
+ - Output directory created automatically if missing
57
+ - Existing files overwritten without warning
58
+
59
+ Examples
60
+ --------
61
+ >>> tearsheet_path = generate_quantstats_tearsheet(
62
+ ... returns=strategy_returns,
63
+ ... benchmark=None,
64
+ ... output_path=Path("reports/performance/strategy_tearsheet.html"),
65
+ ... title="CDX ETF Basis - Simple Threshold",
66
+ ... )
67
+ """
68
+ try:
69
+ # Ensure output directory exists
70
+ output_path = Path(output_path)
71
+ output_path.parent.mkdir(parents=True, exist_ok=True)
72
+
73
+ # Ensure series have names (quantstats requires this)
74
+ if returns.name is None:
75
+ returns = returns.copy()
76
+ returns.name = "Strategy"
77
+ if benchmark is not None and benchmark.name is None:
78
+ benchmark = benchmark.copy()
79
+ benchmark.name = "Benchmark"
80
+
81
+ # Generate tearsheet
82
+ logger.info(
83
+ "Generating quantstats tearsheet: %s (returns=%d, benchmark=%s)",
84
+ output_path.name,
85
+ len(returns),
86
+ "provided" if benchmark is not None else "none",
87
+ )
88
+
89
+ qs.reports.html(
90
+ returns,
91
+ benchmark=benchmark,
92
+ output=str(output_path),
93
+ title=title,
94
+ periods_per_year=periods_per_year,
95
+ )
96
+
97
+ # Clean up matplotlib figures to avoid memory issues
98
+ plt.close("all")
99
+
100
+ logger.info("Saved quantstats tearsheet to %s", output_path)
101
+ return output_path
102
+
103
+ except Exception as e:
104
+ logger.warning(
105
+ "Failed to generate quantstats tearsheet: %s: %s",
106
+ type(e).__name__,
107
+ e,
108
+ )
109
+ return None
110
+
111
+
112
+ def generate_performance_report(
113
+ result: PerformanceResult,
114
+ signal_id: str,
115
+ strategy_id: str,
116
+ generate_tearsheet: bool = True,
117
+ output_dir: Path | None = None,
118
+ ) -> str:
119
+ """
120
+ Generate Markdown report from performance evaluation result.
121
+
122
+ Parameters
123
+ ----------
124
+ result : PerformanceResult
125
+ Performance evaluation result to document.
126
+ signal_id : str
127
+ Signal identifier (for header).
128
+ strategy_id : str
129
+ Strategy identifier (for header).
130
+ generate_tearsheet : bool, default=True
131
+ If True and quantstats available, generate HTML tearsheet alongside report.
132
+ output_dir : Path | None, default=None
133
+ Output directory for tearsheet. Required if generate_tearsheet=True.
134
+ Ignored if generate_tearsheet=False.
135
+
136
+ Returns
137
+ -------
138
+ str
139
+ Formatted Markdown report.
140
+
141
+ Notes
142
+ -----
143
+ Report includes:
144
+ - Header with identifiers and stability score
145
+ - Executive summary with key insights
146
+ - Extended metrics section
147
+ - Subperiod stability analysis
148
+ - Return attribution breakdown
149
+ - Recommendations
150
+ - Footer with metadata
151
+
152
+ Tearsheet generation:
153
+ - Only occurs if generate_tearsheet=True AND quantstats is installed
154
+ - Uses returns from result.metadata['returns'] if available
155
+ - Uses benchmark from result.metadata['benchmark'] if available
156
+ - Silently skips if dependencies unavailable (DEBUG-level logging)
157
+ - Filename format: {signal_id}_{strategy_id}_{timestamp}_tearsheet.html
158
+
159
+ Examples
160
+ --------
161
+ >>> report = generate_performance_report(result, "cdx_etf_basis", "simple_threshold")
162
+ >>> print(report[:100])
163
+ """
164
+ # Stability indicator
165
+ if result.stability_score >= 0.7:
166
+ stability_indicator = "[STRONG]"
167
+ elif result.stability_score >= 0.4:
168
+ stability_indicator = "[MODERATE]"
169
+ else:
170
+ stability_indicator = "[WEAK]"
171
+
172
+ # Extract key metrics (use dataclass field access)
173
+ metrics = result.metrics
174
+ subperiod = result.subperiod_analysis
175
+ attribution = result.attribution
176
+
177
+ # Build report
178
+ report = f"""# Backtest Performance Evaluation Report
179
+
180
+ **Signal:** `{signal_id}`
181
+ **Strategy:** `{strategy_id}`
182
+ **Evaluation Date:** {result.timestamp}
183
+ **Evaluator Version:** {result.metadata.get("evaluator_version", "unknown")}
184
+
185
+ ---
186
+
187
+ ## Executive Summary
188
+
189
+ ### Stability Assessment: {stability_indicator}
190
+
191
+ **Overall Stability Score:** {result.stability_score:.3f}
192
+
193
+ {result.summary}
194
+
195
+ ---
196
+
197
+ ## Basic Backtest Metrics
198
+
199
+ | Metric | Value |
200
+ |--------|-------|
201
+ | Total Return | ${metrics.total_return:,.2f} |
202
+ | Annualized Return | ${metrics.annualized_return:,.2f} |
203
+ | Sharpe Ratio | {metrics.sharpe_ratio:.3f} |
204
+ | Sortino Ratio | {metrics.sortino_ratio:.3f} |
205
+ | Max Drawdown | ${metrics.max_drawdown:,.2f} |
206
+ | Calmar Ratio | {metrics.calmar_ratio:.3f} |
207
+ | Annualized Volatility | ${metrics.annualized_volatility:,.2f} |
208
+
209
+ ### Trade Statistics
210
+
211
+ | Metric | Value |
212
+ |--------|-------|
213
+ | Total Trades | {metrics.n_trades} |
214
+ | Hit Rate | {metrics.hit_rate:.1%} |
215
+ | Average Win | ${metrics.avg_win:,.2f} |
216
+ | Average Loss | ${metrics.avg_loss:,.2f} |
217
+ | Win/Loss Ratio | {metrics.win_loss_ratio:.3f} |
218
+ | Avg Holding Days | {metrics.avg_holding_days:.1f} |
219
+
220
+ ---
221
+
222
+ ## Extended Performance Metrics
223
+
224
+ ### Risk-Adjusted Returns
225
+
226
+ | Metric | Value |
227
+ |--------|-------|
228
+ | Rolling Sharpe (Mean) | {metrics.rolling_sharpe_mean:.3f} |
229
+ | Rolling Sharpe (Std Dev) | {metrics.rolling_sharpe_std:.3f} |
230
+ | Profit Factor | {metrics.profit_factor:.3f} |
231
+ | Tail Ratio (95th pct) | {metrics.tail_ratio:.3f} |
232
+ | Consistency Score (21d) | {metrics.consistency_score:.1%} |
233
+
234
+ **Interpretation:**
235
+
236
+ """
237
+
238
+ # Add metric interpretations
239
+ if metrics.profit_factor > 1.5:
240
+ report += "- Strong profitability with gross wins substantially exceeding gross losses\n"
241
+ elif metrics.profit_factor > 1.0:
242
+ report += "- Positive profitability with gross wins exceeding gross losses\n"
243
+ else:
244
+ report += "- Weak profitability with gross losses approaching or exceeding gross wins\n"
245
+
246
+ if metrics.tail_ratio > 1.2:
247
+ report += (
248
+ "- Favorable tail asymmetry with larger upside than downside extremes\n"
249
+ )
250
+ elif metrics.tail_ratio > 0.8:
251
+ report += (
252
+ "- Balanced tail distribution with similar upside and downside extremes\n"
253
+ )
254
+ else:
255
+ report += (
256
+ "- Negative tail asymmetry with larger downside than upside extremes\n"
257
+ )
258
+
259
+ if metrics.consistency_score > 0.6:
260
+ report += "- High consistency with majority of rolling windows profitable\n"
261
+ elif metrics.consistency_score > 0.4:
262
+ report += "- Moderate consistency with mixed profitable/unprofitable periods\n"
263
+ else:
264
+ report += "- Low consistency with frequent unprofitable rolling windows\n"
265
+
266
+ # Drawdown recovery
267
+ max_dd_recovery = metrics.max_dd_recovery_days
268
+ if max_dd_recovery == float("inf"):
269
+ recovery_text = "Not recovered"
270
+ else:
271
+ recovery_text = f"{max_dd_recovery:.0f} days"
272
+
273
+ report += f"""
274
+ ### Drawdown Recovery
275
+
276
+ | Metric | Value |
277
+ |--------|-------|
278
+ | Max Drawdown Recovery | {recovery_text} |
279
+ | Average Recovery Time | {metrics.avg_recovery_days:.1f} days |
280
+ | Number of Drawdowns | {metrics.n_drawdowns} |
281
+
282
+ """
283
+
284
+ if max_dd_recovery == float("inf"):
285
+ report += "**Warning:** Maximum drawdown has not been recovered as of backtest end date.\n"
286
+
287
+ # Subperiod stability
288
+ report += f"""
289
+ ---
290
+
291
+ ## Subperiod Stability Analysis
292
+
293
+ **Number of Subperiods:** {len(subperiod["subperiod_returns"])}
294
+ **Profitable Periods:** {subperiod["positive_periods"]}/{len(subperiod["subperiod_returns"])}
295
+ **Consistency Rate:** {subperiod["consistency_rate"]:.1%}
296
+
297
+ | Period | Return | Sharpe |
298
+ |--------|--------|--------|
299
+ """
300
+
301
+ for i, (ret, sharpe) in enumerate(
302
+ zip(subperiod["subperiod_returns"], subperiod["subperiod_sharpes"]), 1
303
+ ):
304
+ report += f"| {i} | {ret:,.2f} | {sharpe:.3f} |\n"
305
+
306
+ report += "\n**Interpretation:**\n\n"
307
+
308
+ if subperiod["consistency_rate"] >= 0.75:
309
+ report += (
310
+ "Excellent temporal consistency with strong performance across most subperiods. "
311
+ "Strategy appears robust to different market conditions.\n"
312
+ )
313
+ elif subperiod["consistency_rate"] >= 0.5:
314
+ report += (
315
+ "Moderate temporal consistency with mixed performance across subperiods. "
316
+ "Performance may be regime-dependent.\n"
317
+ )
318
+ else:
319
+ report += (
320
+ "Weak temporal consistency with performance concentrated in few subperiods. "
321
+ "Strategy may be vulnerable to regime changes or overfitted to specific conditions.\n"
322
+ )
323
+
324
+ # Return attribution
325
+ direction = attribution["direction"]
326
+ signal_strength = attribution["signal_strength"]
327
+ win_loss = attribution["win_loss"]
328
+
329
+ report += f"""
330
+ ---
331
+
332
+ ## Return Attribution
333
+
334
+ ### Directional Attribution
335
+
336
+ | Direction | P&L | Contribution |
337
+ |-----------|-----|--------------|
338
+ | Long | {direction["long_pnl"]:,.2f} | {direction["long_pct"]:.1%} |
339
+ | Short | {direction["short_pnl"]:,.2f} | {direction["short_pct"]:.1%} |
340
+
341
+ """
342
+
343
+ if abs(direction["long_pct"]) > 0.7:
344
+ bias = "long" if direction["long_pct"] > 0 else "short"
345
+ report += f"**Strong {bias} bias** - Returns highly concentrated in {bias} positions.\n"
346
+ else:
347
+ report += "**Balanced exposure** - Returns distributed across both long and short positions.\n"
348
+
349
+ report += "\n### Signal Strength Attribution\n\n"
350
+ report += "| Quantile | P&L | Contribution |\n"
351
+ report += "|----------|-----|--------------|\n"
352
+
353
+ n_quantiles = result.config.attribution_quantiles
354
+ for i in range(1, n_quantiles + 1):
355
+ pnl = signal_strength[f"q{i}_pnl"]
356
+ pct = signal_strength[f"q{i}_pct"]
357
+ report += f"| Q{i} | {pnl:,.2f} | {pct:.1%} |\n"
358
+
359
+ report += "\n"
360
+
361
+ # Check if highest quantile contributed most
362
+ highest_q_pct = signal_strength[f"q{n_quantiles}_pct"]
363
+ if highest_q_pct > 0.4:
364
+ report += (
365
+ "**Strong signal strength relationship** - Highest conviction signals contributed "
366
+ f"most to returns ({highest_q_pct:.1%}).\n"
367
+ )
368
+ elif highest_q_pct < 0.2:
369
+ report += (
370
+ "**Weak signal strength relationship** - Returns not concentrated in highest "
371
+ "conviction signals. Signal strength may not add value.\n"
372
+ )
373
+ else:
374
+ report += "**Moderate signal strength relationship** - Mixed contribution across signal strengths.\n"
375
+
376
+ report += f"""
377
+ ### Win/Loss Decomposition
378
+
379
+ | Category | Amount | Contribution |
380
+ |----------|--------|--------------|
381
+ | Gross Wins | {win_loss["gross_wins"]:,.2f} | {win_loss["win_contribution"]:.1%} |
382
+ | Gross Losses | {win_loss["gross_losses"]:,.2f} | {win_loss["loss_contribution"]:.1%} |
383
+ | Net P&L | {win_loss["net_pnl"]:,.2f} | — |
384
+
385
+ ---
386
+
387
+ ## Recommendations
388
+
389
+ """
390
+
391
+ # Generate recommendations based on metrics
392
+ recommendations = []
393
+
394
+ if result.stability_score < 0.5:
395
+ recommendations.append(
396
+ "[WARNING] **Low stability score** - Review strategy robustness and consider regime filters"
397
+ )
398
+
399
+ if metrics.profit_factor < 1.0:
400
+ recommendations.append(
401
+ "[FAIL] **Negative profit factor** - Strategy is unprofitable; do not deploy"
402
+ )
403
+
404
+ if subperiod["consistency_rate"] < 0.5:
405
+ recommendations.append(
406
+ "[WARNING] **Low consistency** - Performance concentrated in few periods; assess regime dependency"
407
+ )
408
+
409
+ if max_dd_recovery == float("inf"):
410
+ recommendations.append(
411
+ "[WARNING] **Unrecovered drawdown** - Current strategy underwater; reassess viability"
412
+ )
413
+
414
+ if metrics.tail_ratio < 0.8:
415
+ recommendations.append(
416
+ "[WARNING] **Negative skew** - Downside risk exceeds upside potential; review risk controls"
417
+ )
418
+
419
+ if not recommendations:
420
+ recommendations.append(
421
+ "[PASS] **Performance acceptable** - Consider proceeding with further validation and stress testing"
422
+ )
423
+ recommendations.append(
424
+ "Next steps: comparative analysis against alternative signals/strategies"
425
+ )
426
+ recommendations.append(
427
+ "Recommended: transaction cost sensitivity analysis and regime-conditional performance review"
428
+ )
429
+
430
+ for rec in recommendations:
431
+ report += f"{rec}\n\n"
432
+
433
+ report += f"""
434
+ ---
435
+
436
+ ## Report Metadata
437
+
438
+ **Generated:** {datetime.now().isoformat()}
439
+ **Evaluator:** aponyx.evaluation.performance
440
+ **Configuration:**
441
+ - Minimum Observations: {result.config.min_obs}
442
+ - Subperiods: {result.config.n_subperiods}
443
+ - Rolling Window: {result.config.rolling_window} days
444
+ - Attribution Quantiles: {result.config.attribution_quantiles}
445
+
446
+ **Reproducibility:** All metrics computed from backtest P&L with deterministic methods.
447
+
448
+ ---
449
+
450
+ *This report was auto-generated from performance evaluation results.*
451
+ """
452
+
453
+ logger.debug(
454
+ "Generated performance report for %s/%s: %d characters",
455
+ signal_id,
456
+ strategy_id,
457
+ len(report),
458
+ )
459
+
460
+ # Generate quantstats tearsheet if requested
461
+ if generate_tearsheet and output_dir is not None:
462
+ returns = result.metadata.get("returns")
463
+ benchmark = result.metadata.get("benchmark")
464
+
465
+ if returns is not None:
466
+ timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
467
+ tearsheet_filename = (
468
+ f"{signal_id}_{strategy_id}_{timestamp_str}_tearsheet.html"
469
+ )
470
+ tearsheet_path = Path(output_dir) / tearsheet_filename
471
+ title = f"{signal_id} - {strategy_id}"
472
+
473
+ generate_quantstats_tearsheet(
474
+ returns=returns,
475
+ benchmark=benchmark,
476
+ output_path=tearsheet_path,
477
+ title=title,
478
+ )
479
+ else:
480
+ logger.debug(
481
+ "Skipping tearsheet generation - returns not found in result.metadata"
482
+ )
483
+
484
+ return report
485
+
486
+
487
+ def save_report(
488
+ report: str,
489
+ signal_id: str,
490
+ strategy_id: str,
491
+ output_dir: Path,
492
+ timestamp: str | None = None,
493
+ ) -> Path:
494
+ """
495
+ Save report to Markdown file.
496
+
497
+ Parameters
498
+ ----------
499
+ report : str
500
+ Markdown report text.
501
+ signal_id : str
502
+ Signal identifier (for filename).
503
+ strategy_id : str
504
+ Strategy identifier (for filename).
505
+ output_dir : Path
506
+ Directory to save report.
507
+ timestamp : str or None, optional
508
+ Timestamp string (YYYYMMDD_HHMMSS). If None, generates new timestamp.
509
+
510
+ Returns
511
+ -------
512
+ Path
513
+ Path to saved report file.
514
+
515
+ Notes
516
+ -----
517
+ Filename format: performance_analysis_{YYYYMMDD_HHMMSS}.md
518
+ Creates output directory if it doesn't exist.
519
+
520
+ Examples
521
+ --------
522
+ >>> from aponyx.config import PERFORMANCE_REPORTS_DIR
523
+ >>> path = save_report(report, "cdx_etf_basis", "simple_threshold", PERFORMANCE_REPORTS_DIR)
524
+ >>> print(path)
525
+ """
526
+ # Create output directory
527
+ output_dir = Path(output_dir)
528
+ output_dir.mkdir(parents=True, exist_ok=True)
529
+
530
+ # Generate or use provided timestamp
531
+ if timestamp is None:
532
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
533
+ filename = f"performance_analysis_{timestamp}.md"
534
+ output_path = output_dir / filename
535
+
536
+ # Write report
537
+ output_path.write_text(report, encoding="utf-8")
538
+
539
+ logger.info("Saved performance report to %s", output_path)
540
+
541
+ return output_path
@@ -0,0 +1,67 @@
1
+ """
2
+ Signal-product suitability evaluation module.
3
+
4
+ This module provides tools to evaluate whether signals contain economically
5
+ and statistically meaningful information for traded products. Acts as a
6
+ research screening gate before proceeding to strategy design and backtesting.
7
+
8
+ Public API
9
+ ----------
10
+ - evaluate_signal_suitability: Main evaluation function
11
+ - SuitabilityConfig: Configuration dataclass
12
+ - SuitabilityResult: Result dataclass
13
+ - SuitabilityRegistry: Evaluation tracking with CRUD operations
14
+ - EvaluationEntry: Registry entry dataclass
15
+ - generate_suitability_report: Markdown report generator
16
+ - save_report: Report file persistence
17
+ - compute_forward_returns: Target construction utility
18
+
19
+ Examples
20
+ --------
21
+ >>> from aponyx.evaluation.suitability import (
22
+ ... evaluate_signal_suitability,
23
+ ... SuitabilityConfig,
24
+ ... generate_suitability_report,
25
+ ... save_report,
26
+ ... SuitabilityRegistry,
27
+ ... )
28
+ >>> from aponyx.config import EVALUATION_DIR, SUITABILITY_REGISTRY_PATH
29
+ >>>
30
+ >>> # Evaluate signal
31
+ >>> result = evaluate_signal_suitability(signal, target_change)
32
+ >>> print(result.decision, result.composite_score)
33
+ >>>
34
+ >>> # Generate report
35
+ >>> report = generate_suitability_report(result, "cdx_etf_basis", "cdx_ig_5y")
36
+ >>> report_path = save_report(report, "cdx_etf_basis", "cdx_ig_5y", EVALUATION_DIR)
37
+ >>>
38
+ >>> # Register evaluation
39
+ >>> registry = SuitabilityRegistry(SUITABILITY_REGISTRY_PATH)
40
+ >>> eval_id = registry.register_evaluation(result, "cdx_etf_basis", "cdx_ig_5y")
41
+ """
42
+
43
+ from aponyx.evaluation.suitability.config import SuitabilityConfig
44
+ from aponyx.evaluation.suitability.evaluator import (
45
+ SuitabilityResult,
46
+ evaluate_signal_suitability,
47
+ compute_forward_returns,
48
+ )
49
+ from aponyx.evaluation.suitability.registry import (
50
+ SuitabilityRegistry,
51
+ EvaluationEntry,
52
+ )
53
+ from aponyx.evaluation.suitability.report import (
54
+ generate_suitability_report,
55
+ save_report,
56
+ )
57
+
58
+ __all__ = [
59
+ "SuitabilityConfig",
60
+ "SuitabilityResult",
61
+ "evaluate_signal_suitability",
62
+ "compute_forward_returns",
63
+ "SuitabilityRegistry",
64
+ "EvaluationEntry",
65
+ "generate_suitability_report",
66
+ "save_report",
67
+ ]