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.
- aponyx/__init__.py +14 -0
- aponyx/backtest/__init__.py +31 -0
- aponyx/backtest/adapters.py +77 -0
- aponyx/backtest/config.py +84 -0
- aponyx/backtest/engine.py +560 -0
- aponyx/backtest/protocols.py +101 -0
- aponyx/backtest/registry.py +334 -0
- aponyx/backtest/strategy_catalog.json +50 -0
- aponyx/cli/__init__.py +5 -0
- aponyx/cli/commands/__init__.py +8 -0
- aponyx/cli/commands/clean.py +349 -0
- aponyx/cli/commands/list.py +302 -0
- aponyx/cli/commands/report.py +167 -0
- aponyx/cli/commands/run.py +377 -0
- aponyx/cli/main.py +125 -0
- aponyx/config/__init__.py +82 -0
- aponyx/data/__init__.py +99 -0
- aponyx/data/bloomberg_config.py +306 -0
- aponyx/data/bloomberg_instruments.json +26 -0
- aponyx/data/bloomberg_securities.json +42 -0
- aponyx/data/cache.py +294 -0
- aponyx/data/fetch.py +659 -0
- aponyx/data/fetch_registry.py +135 -0
- aponyx/data/loaders.py +205 -0
- aponyx/data/providers/__init__.py +13 -0
- aponyx/data/providers/bloomberg.py +383 -0
- aponyx/data/providers/file.py +111 -0
- aponyx/data/registry.py +500 -0
- aponyx/data/requirements.py +96 -0
- aponyx/data/sample_data.py +415 -0
- aponyx/data/schemas.py +60 -0
- aponyx/data/sources.py +171 -0
- aponyx/data/synthetic_params.json +46 -0
- aponyx/data/transforms.py +336 -0
- aponyx/data/validation.py +308 -0
- aponyx/docs/__init__.py +24 -0
- aponyx/docs/adding_data_providers.md +682 -0
- aponyx/docs/cdx_knowledge_base.md +455 -0
- aponyx/docs/cdx_overlay_strategy.md +135 -0
- aponyx/docs/cli_guide.md +607 -0
- aponyx/docs/governance_design.md +551 -0
- aponyx/docs/logging_design.md +251 -0
- aponyx/docs/performance_evaluation_design.md +265 -0
- aponyx/docs/python_guidelines.md +786 -0
- aponyx/docs/signal_registry_usage.md +369 -0
- aponyx/docs/signal_suitability_design.md +558 -0
- aponyx/docs/visualization_design.md +277 -0
- aponyx/evaluation/__init__.py +11 -0
- aponyx/evaluation/performance/__init__.py +24 -0
- aponyx/evaluation/performance/adapters.py +109 -0
- aponyx/evaluation/performance/analyzer.py +384 -0
- aponyx/evaluation/performance/config.py +320 -0
- aponyx/evaluation/performance/decomposition.py +304 -0
- aponyx/evaluation/performance/metrics.py +761 -0
- aponyx/evaluation/performance/registry.py +327 -0
- aponyx/evaluation/performance/report.py +541 -0
- aponyx/evaluation/suitability/__init__.py +67 -0
- aponyx/evaluation/suitability/config.py +143 -0
- aponyx/evaluation/suitability/evaluator.py +389 -0
- aponyx/evaluation/suitability/registry.py +328 -0
- aponyx/evaluation/suitability/report.py +398 -0
- aponyx/evaluation/suitability/scoring.py +367 -0
- aponyx/evaluation/suitability/tests.py +303 -0
- aponyx/examples/01_generate_synthetic_data.py +53 -0
- aponyx/examples/02_fetch_data_file.py +82 -0
- aponyx/examples/03_fetch_data_bloomberg.py +104 -0
- aponyx/examples/04_compute_signal.py +164 -0
- aponyx/examples/05_evaluate_suitability.py +224 -0
- aponyx/examples/06_run_backtest.py +242 -0
- aponyx/examples/07_analyze_performance.py +214 -0
- aponyx/examples/08_visualize_results.py +272 -0
- aponyx/main.py +7 -0
- aponyx/models/__init__.py +45 -0
- aponyx/models/config.py +83 -0
- aponyx/models/indicator_transformation.json +52 -0
- aponyx/models/indicators.py +292 -0
- aponyx/models/metadata.py +447 -0
- aponyx/models/orchestrator.py +213 -0
- aponyx/models/registry.py +860 -0
- aponyx/models/score_transformation.json +42 -0
- aponyx/models/signal_catalog.json +29 -0
- aponyx/models/signal_composer.py +513 -0
- aponyx/models/signal_transformation.json +29 -0
- aponyx/persistence/__init__.py +16 -0
- aponyx/persistence/json_io.py +132 -0
- aponyx/persistence/parquet_io.py +378 -0
- aponyx/py.typed +0 -0
- aponyx/reporting/__init__.py +10 -0
- aponyx/reporting/generator.py +517 -0
- aponyx/visualization/__init__.py +20 -0
- aponyx/visualization/app.py +37 -0
- aponyx/visualization/plots.py +309 -0
- aponyx/visualization/visualizer.py +242 -0
- aponyx/workflows/__init__.py +18 -0
- aponyx/workflows/concrete_steps.py +720 -0
- aponyx/workflows/config.py +122 -0
- aponyx/workflows/engine.py +279 -0
- aponyx/workflows/registry.py +116 -0
- aponyx/workflows/steps.py +180 -0
- aponyx-0.1.18.dist-info/METADATA +552 -0
- aponyx-0.1.18.dist-info/RECORD +104 -0
- aponyx-0.1.18.dist-info/WHEEL +4 -0
- aponyx-0.1.18.dist-info/entry_points.txt +2 -0
- aponyx-0.1.18.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown report generation for suitability evaluation results.
|
|
3
|
+
|
|
4
|
+
Generates human-readable reports with evaluation metrics, scores,
|
|
5
|
+
and interpretation guidance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from aponyx.evaluation.suitability.evaluator import SuitabilityResult
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_suitability_report(
|
|
18
|
+
result: SuitabilityResult,
|
|
19
|
+
signal_id: str,
|
|
20
|
+
product_id: str,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Generate Markdown report from evaluation result.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
result : SuitabilityResult
|
|
28
|
+
Evaluation result to document.
|
|
29
|
+
signal_id : str
|
|
30
|
+
Signal identifier (for header).
|
|
31
|
+
product_id : str
|
|
32
|
+
Product identifier matching security_id format (e.g., 'cdx_ig_5y').
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
str
|
|
37
|
+
Formatted Markdown report.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
Report includes:
|
|
42
|
+
- Header with identifiers and overall decision
|
|
43
|
+
- Executive summary with composite score
|
|
44
|
+
- Four component sections with metrics and interpretation
|
|
45
|
+
- Composite scoring breakdown
|
|
46
|
+
- Decision explanation and next steps
|
|
47
|
+
- Footer with metadata
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> report = generate_suitability_report(result, "cdx_etf_basis", "cdx_ig_5y")
|
|
52
|
+
>>> print(report[:100])
|
|
53
|
+
"""
|
|
54
|
+
# Decision indicator
|
|
55
|
+
if result.decision == "PASS":
|
|
56
|
+
indicator = "[PASS]"
|
|
57
|
+
elif result.decision == "HOLD":
|
|
58
|
+
indicator = "[HOLD]"
|
|
59
|
+
else:
|
|
60
|
+
indicator = "[FAIL]"
|
|
61
|
+
|
|
62
|
+
# Interpretation text for composite score
|
|
63
|
+
if result.composite_score >= 0.7:
|
|
64
|
+
interpretation = (
|
|
65
|
+
"The signal demonstrates strong predictive content with good data quality. "
|
|
66
|
+
"Proceed to strategy design and backtesting."
|
|
67
|
+
)
|
|
68
|
+
elif result.composite_score >= 0.4:
|
|
69
|
+
interpretation = (
|
|
70
|
+
"The signal shows marginal predictive content. "
|
|
71
|
+
"Consider refining signal construction or gathering more data before backtesting. "
|
|
72
|
+
"Manual review recommended."
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
interpretation = (
|
|
76
|
+
"The signal lacks sufficient predictive content for this product. "
|
|
77
|
+
"Do not proceed to backtesting. Consider alternative signal specifications."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Component interpretations
|
|
81
|
+
data_health_interp = _interpret_data_health(result)
|
|
82
|
+
predictive_interp = _interpret_predictive(result)
|
|
83
|
+
economic_interp = _interpret_economic(result)
|
|
84
|
+
stability_interp = _interpret_stability(result)
|
|
85
|
+
|
|
86
|
+
# Build report
|
|
87
|
+
report = f"""# Signal-Product Suitability Evaluation Report
|
|
88
|
+
|
|
89
|
+
**Signal:** `{signal_id}`
|
|
90
|
+
**Product:** `{product_id}`
|
|
91
|
+
**Evaluation Date:** {result.timestamp}
|
|
92
|
+
**Evaluator Version:** 0.1.0
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Executive Summary
|
|
97
|
+
|
|
98
|
+
### Overall Decision: {indicator}
|
|
99
|
+
|
|
100
|
+
**Composite Score:** {result.composite_score:.3f}
|
|
101
|
+
|
|
102
|
+
{interpretation}
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Component Analysis
|
|
107
|
+
|
|
108
|
+
### 1. Data Health Score: {result.data_health_score:.3f}
|
|
109
|
+
|
|
110
|
+
**Metrics:**
|
|
111
|
+
- Valid Observations: {result.valid_obs:,}
|
|
112
|
+
- Missing Data: {result.missing_pct:.2f}%
|
|
113
|
+
|
|
114
|
+
**Interpretation:**
|
|
115
|
+
{data_health_interp}
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### 2. Predictive Association Score: {result.predictive_score:.3f}
|
|
120
|
+
|
|
121
|
+
**Metrics:**
|
|
122
|
+
|
|
123
|
+
| Lag | Correlation | Beta | T-Statistic |
|
|
124
|
+
|-----|-------------|------|-------------|
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Add stats for each lag
|
|
128
|
+
for lag in sorted(result.correlations.keys()):
|
|
129
|
+
corr = result.correlations.get(lag, 0.0)
|
|
130
|
+
beta = result.betas.get(lag, 0.0)
|
|
131
|
+
tstat = result.t_stats.get(lag, 0.0)
|
|
132
|
+
report += f"| {lag} | {corr:.4f} | {beta:.4f} | {tstat:.4f} |\n"
|
|
133
|
+
|
|
134
|
+
report += f"""
|
|
135
|
+
**Interpretation:**
|
|
136
|
+
{predictive_interp}
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### 3. Economic Relevance Score: {result.economic_score:.3f}
|
|
141
|
+
|
|
142
|
+
**Metrics:**
|
|
143
|
+
- Effect Size: {result.effect_size_bps:.3f} bps per 1σ signal change
|
|
144
|
+
|
|
145
|
+
**Interpretation:**
|
|
146
|
+
{economic_interp}
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### 4. Temporal Stability Score: {result.stability_score:.3f}
|
|
151
|
+
|
|
152
|
+
**Metrics:**
|
|
153
|
+
- Rolling Windows: {result.n_windows} windows ({result.config.rolling_window} observations each)
|
|
154
|
+
- Sign Consistency Ratio: {result.sign_consistency_ratio:.1%}
|
|
155
|
+
- Beta Coefficient of Variation: {result.beta_cv:.3f}
|
|
156
|
+
|
|
157
|
+
**Interpretation:**
|
|
158
|
+
{stability_interp}
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Composite Scoring
|
|
163
|
+
|
|
164
|
+
| Component | Weight | Score | Contribution |
|
|
165
|
+
|-----------|--------|-------|--------------|
|
|
166
|
+
| Data Health | {result.config.data_health_weight:.2f} | {result.data_health_score:.3f} | {result.config.data_health_weight * result.data_health_score:.3f} |
|
|
167
|
+
| Predictive | {result.config.predictive_weight:.2f} | {result.predictive_score:.3f} | {result.config.predictive_weight * result.predictive_score:.3f} |
|
|
168
|
+
| Economic | {result.config.economic_weight:.2f} | {result.economic_score:.3f} | {result.config.economic_weight * result.economic_score:.3f} |
|
|
169
|
+
| Stability | {result.config.stability_weight:.2f} | {result.stability_score:.3f} | {result.config.stability_weight * result.stability_score:.3f} |
|
|
170
|
+
| **Total** | **1.00** | — | **{result.composite_score:.3f}** |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Decision Criteria
|
|
175
|
+
|
|
176
|
+
- **PASS** (≥ {result.config.pass_threshold:.2f}): Proceed to backtest
|
|
177
|
+
- **HOLD** ({result.config.hold_threshold:.2f} - {result.config.pass_threshold:.2f}): Marginal, requires judgment
|
|
178
|
+
- **FAIL** (< {result.config.hold_threshold:.2f}): Do not backtest
|
|
179
|
+
|
|
180
|
+
### Recommended Next Steps
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if result.decision == "PASS":
|
|
185
|
+
report += """1. Design trading strategy with entry/exit rules
|
|
186
|
+
2. Configure backtest parameters (position sizing, costs)
|
|
187
|
+
3. Run historical backtest with proper risk controls
|
|
188
|
+
4. Analyze performance metrics and risk-adjusted returns
|
|
189
|
+
"""
|
|
190
|
+
elif result.decision == "HOLD":
|
|
191
|
+
report += """1. Review component scores to identify weaknesses
|
|
192
|
+
2. Consider signal refinements (lookback periods, normalization)
|
|
193
|
+
3. Gather additional data if sample size is limited
|
|
194
|
+
4. Consult with senior researchers before proceeding
|
|
195
|
+
5. Document rationale for proceed/stop decision
|
|
196
|
+
"""
|
|
197
|
+
else:
|
|
198
|
+
report += """1. Archive evaluation for reference
|
|
199
|
+
2. Document why signal failed (data, predictive, economic, or stability)
|
|
200
|
+
3. Consider alternative signal specifications
|
|
201
|
+
4. Do NOT proceed to backtesting with current signal
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
report += f"""
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Report Metadata
|
|
208
|
+
|
|
209
|
+
**Generated:** {datetime.now().isoformat()}
|
|
210
|
+
**Evaluator:** aponyx.evaluation.suitability v0.1.0
|
|
211
|
+
**Reproducibility:** All metrics computed from aligned signal-target pairs with deterministic methods.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
*This report was auto-generated from suitability evaluation results. For questions about methodology, see `docs/suitability_evaluation.md`.*
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
logger.debug(
|
|
219
|
+
"Generated report for %s/%s: %d characters",
|
|
220
|
+
signal_id,
|
|
221
|
+
product_id,
|
|
222
|
+
len(report),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return report
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _interpret_data_health(result: SuitabilityResult) -> str:
|
|
229
|
+
"""Generate interpretation text for data health component."""
|
|
230
|
+
if result.data_health_score >= 0.8:
|
|
231
|
+
return (
|
|
232
|
+
"Excellent data quality with sufficient observations and minimal missing data. "
|
|
233
|
+
"Sample size supports reliable statistical inference."
|
|
234
|
+
)
|
|
235
|
+
elif result.data_health_score >= 0.5:
|
|
236
|
+
return (
|
|
237
|
+
"Acceptable data quality with some missing data. "
|
|
238
|
+
"Results should be interpreted with awareness of data limitations."
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
return (
|
|
242
|
+
"Data quality concerns due to insufficient observations or high missing data rate. "
|
|
243
|
+
"Results may not be reliable. Consider gathering more data."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _interpret_predictive(result: SuitabilityResult) -> str:
|
|
248
|
+
"""Generate interpretation text for predictive component."""
|
|
249
|
+
mean_abs_tstat = (
|
|
250
|
+
sum(abs(t) for t in result.t_stats.values()) / len(result.t_stats)
|
|
251
|
+
if result.t_stats
|
|
252
|
+
else 0.0
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if mean_abs_tstat >= 3.0:
|
|
256
|
+
return (
|
|
257
|
+
"Strong statistical evidence of predictive relationship. "
|
|
258
|
+
"T-statistics exceed conventional significance thresholds with high confidence."
|
|
259
|
+
)
|
|
260
|
+
elif mean_abs_tstat >= 2.0:
|
|
261
|
+
return (
|
|
262
|
+
"Statistically significant predictive relationship at conventional levels (95% confidence). "
|
|
263
|
+
"Signal contains meaningful information about target movements."
|
|
264
|
+
)
|
|
265
|
+
elif mean_abs_tstat >= 1.5:
|
|
266
|
+
return (
|
|
267
|
+
"Weak but detectable statistical relationship. "
|
|
268
|
+
"Signal may contain information, but evidence is marginal."
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
return (
|
|
272
|
+
"No statistically significant predictive relationship detected. "
|
|
273
|
+
"Signal appears uncorrelated with target movements."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _interpret_economic(result: SuitabilityResult) -> str:
|
|
278
|
+
"""Generate interpretation text for economic component."""
|
|
279
|
+
if result.effect_size_bps >= 2.0:
|
|
280
|
+
return (
|
|
281
|
+
"Economically meaningful effect size. "
|
|
282
|
+
"A 1σ signal move is associated with substantial spread changes that could generate "
|
|
283
|
+
"attractive risk-adjusted returns after costs."
|
|
284
|
+
)
|
|
285
|
+
elif result.effect_size_bps >= 0.5:
|
|
286
|
+
return (
|
|
287
|
+
"Moderate economic impact. "
|
|
288
|
+
"Effect size is detectable but may be marginal after transaction costs. "
|
|
289
|
+
"Careful strategy design required."
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
return (
|
|
293
|
+
"Negligible economic impact. "
|
|
294
|
+
"Even if statistically significant, the effect size is too small to generate "
|
|
295
|
+
"meaningful P&L after realistic transaction costs."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _interpret_stability(result: SuitabilityResult) -> str:
|
|
300
|
+
"""Generate interpretation text for stability component."""
|
|
301
|
+
sign_ratio = result.sign_consistency_ratio
|
|
302
|
+
cv = result.beta_cv
|
|
303
|
+
n_windows = result.n_windows
|
|
304
|
+
|
|
305
|
+
# Interpret sign consistency
|
|
306
|
+
if sign_ratio >= 0.8:
|
|
307
|
+
sign_interp = "highly consistent"
|
|
308
|
+
elif sign_ratio >= 0.6:
|
|
309
|
+
sign_interp = "moderately consistent"
|
|
310
|
+
else:
|
|
311
|
+
sign_interp = "inconsistent"
|
|
312
|
+
|
|
313
|
+
# Interpret magnitude stability
|
|
314
|
+
if cv < 0.5:
|
|
315
|
+
mag_interp = "stable magnitude"
|
|
316
|
+
elif cv < 1.0:
|
|
317
|
+
mag_interp = "moderate variation"
|
|
318
|
+
else:
|
|
319
|
+
mag_interp = "high variation"
|
|
320
|
+
|
|
321
|
+
# Overall interpretation
|
|
322
|
+
if result.stability_score >= 0.8:
|
|
323
|
+
overall = (
|
|
324
|
+
f"Excellent temporal stability ({sign_interp} sign, {mag_interp}). "
|
|
325
|
+
f"The predictive relationship maintains consistent direction and magnitude "
|
|
326
|
+
f"across {n_windows} rolling windows, indicating robustness across different market regimes."
|
|
327
|
+
)
|
|
328
|
+
elif result.stability_score >= 0.5:
|
|
329
|
+
overall = (
|
|
330
|
+
f"Moderate temporal stability ({sign_interp} sign, {mag_interp}). "
|
|
331
|
+
f"The relationship shows some consistency but exhibits regime-dependent behavior. "
|
|
332
|
+
f"Consider investigating the source of variation before strategy design."
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
overall = (
|
|
336
|
+
f"Low temporal stability ({sign_interp} sign, {mag_interp}). "
|
|
337
|
+
f"The predictive relationship is unstable across time, suggesting strong regime "
|
|
338
|
+
f"dependence or non-stationarity. Use caution when designing strategies."
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return overall
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def save_report(
|
|
345
|
+
report: str,
|
|
346
|
+
signal_id: str,
|
|
347
|
+
product_id: str,
|
|
348
|
+
output_dir: Path,
|
|
349
|
+
timestamp: str | None = None,
|
|
350
|
+
) -> Path:
|
|
351
|
+
"""
|
|
352
|
+
Save report to Markdown file.
|
|
353
|
+
|
|
354
|
+
Parameters
|
|
355
|
+
----------
|
|
356
|
+
report : str
|
|
357
|
+
Markdown report text.
|
|
358
|
+
signal_id : str
|
|
359
|
+
Signal identifier (for filename).
|
|
360
|
+
product_id : str
|
|
361
|
+
Product identifier matching security_id format (e.g., 'cdx_ig_5y').
|
|
362
|
+
output_dir : Path
|
|
363
|
+
Directory to save report.
|
|
364
|
+
timestamp : str or None, optional
|
|
365
|
+
Timestamp string (YYYYMMDD_HHMMSS). If None, generates new timestamp.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
Path
|
|
370
|
+
Path to saved report file.
|
|
371
|
+
|
|
372
|
+
Notes
|
|
373
|
+
-----
|
|
374
|
+
Filename format: suitability_evaluation_{YYYYMMDD_HHMMSS}.md
|
|
375
|
+
Creates output directory if it doesn't exist.
|
|
376
|
+
|
|
377
|
+
Examples
|
|
378
|
+
--------
|
|
379
|
+
>>> from aponyx.config import EVALUATION_DIR
|
|
380
|
+
>>> path = save_report(report, "cdx_etf_basis", "cdx_ig_5y", EVALUATION_DIR)
|
|
381
|
+
>>> print(path)
|
|
382
|
+
"""
|
|
383
|
+
# Create output directory
|
|
384
|
+
output_dir = Path(output_dir)
|
|
385
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
|
|
387
|
+
# Generate or use provided timestamp
|
|
388
|
+
if timestamp is None:
|
|
389
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
390
|
+
filename = f"suitability_evaluation_{timestamp}.md"
|
|
391
|
+
output_path = output_dir / filename
|
|
392
|
+
|
|
393
|
+
# Write report
|
|
394
|
+
output_path.write_text(report, encoding="utf-8")
|
|
395
|
+
|
|
396
|
+
logger.info("Saved report to %s", output_path)
|
|
397
|
+
|
|
398
|
+
return output_path
|