sigma-terminal 2.0.1__py3-none-any.whl → 3.2.0__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.
- sigma/__init__.py +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +563 -898
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +434 -0
- sigma/comparison.py +611 -0
- sigma/config.py +195 -0
- sigma/core/__init__.py +4 -17
- sigma/core/engine.py +493 -0
- sigma/core/intent.py +595 -0
- sigma/core/models.py +516 -125
- sigma/data/__init__.py +681 -0
- sigma/data/models.py +130 -0
- sigma/llm.py +401 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +305 -402
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.2.0.dist-info/METADATA +298 -0
- sigma_terminal-3.2.0.dist-info/RECORD +30 -0
- sigma_terminal-3.2.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.2.0.dist-info/licenses/LICENSE +25 -0
- sigma/core/agent.py +0 -205
- sigma/core/config.py +0 -119
- sigma/core/llm.py +0 -794
- sigma/tools/__init__.py +0 -5
- sigma/tools/charts.py +0 -400
- sigma/tools/financial.py +0 -1457
- sigma/ui/__init__.py +0 -1
- sigma_terminal-2.0.1.dist-info/METADATA +0 -222
- sigma_terminal-2.0.1.dist-info/RECORD +0 -19
- sigma_terminal-2.0.1.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.1.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.1.dist-info → sigma_terminal-3.2.0.dist-info}/WHEEL +0 -0
sigma/reporting.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"""Reporting system - Research memos, exports, reproducibility."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime, date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# DATA MODELS
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
class ResearchMemo(BaseModel):
|
|
18
|
+
"""Structured research memo."""
|
|
19
|
+
|
|
20
|
+
id: str = Field(default_factory=lambda: datetime.now().strftime("%Y%m%d_%H%M%S"))
|
|
21
|
+
title: str
|
|
22
|
+
thesis: str
|
|
23
|
+
key_findings: List[str]
|
|
24
|
+
data_sources: List[str]
|
|
25
|
+
methodology: str
|
|
26
|
+
results: Dict[str, Any]
|
|
27
|
+
conclusions: List[str]
|
|
28
|
+
risks_and_limitations: List[str]
|
|
29
|
+
recommendations: List[str]
|
|
30
|
+
appendix: Optional[Dict[str, Any]] = None
|
|
31
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
32
|
+
author: str = "Sigma"
|
|
33
|
+
tags: List[str] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExportConfig(BaseModel):
|
|
37
|
+
"""Configuration for exports."""
|
|
38
|
+
|
|
39
|
+
format: str = "markdown" # markdown, html, pdf, json
|
|
40
|
+
include_charts: bool = True
|
|
41
|
+
include_data: bool = True
|
|
42
|
+
include_code: bool = False
|
|
43
|
+
chart_format: str = "png" # png, svg, html
|
|
44
|
+
data_format: str = "csv" # csv, excel, parquet
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReproducibilityConfig(BaseModel):
|
|
48
|
+
"""Configuration for reproducibility."""
|
|
49
|
+
|
|
50
|
+
query: str
|
|
51
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
|
52
|
+
data_hash: Optional[str] = None
|
|
53
|
+
parameters: Dict[str, Any] = Field(default_factory=dict)
|
|
54
|
+
data_sources: List[str] = Field(default_factory=list)
|
|
55
|
+
random_seed: Optional[int] = None
|
|
56
|
+
versions: Dict[str, str] = Field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ============================================================================
|
|
60
|
+
# MEMO GENERATOR
|
|
61
|
+
# ============================================================================
|
|
62
|
+
|
|
63
|
+
class MemoGenerator:
|
|
64
|
+
"""Generate research memos from analysis results."""
|
|
65
|
+
|
|
66
|
+
MEMO_TEMPLATES = {
|
|
67
|
+
"stock_analysis": """
|
|
68
|
+
# {title}
|
|
69
|
+
|
|
70
|
+
**Date:** {date}
|
|
71
|
+
**Author:** {author}
|
|
72
|
+
|
|
73
|
+
## Executive Summary
|
|
74
|
+
|
|
75
|
+
{thesis}
|
|
76
|
+
|
|
77
|
+
## Key Findings
|
|
78
|
+
|
|
79
|
+
{key_findings}
|
|
80
|
+
|
|
81
|
+
## Methodology
|
|
82
|
+
|
|
83
|
+
{methodology}
|
|
84
|
+
|
|
85
|
+
## Results
|
|
86
|
+
|
|
87
|
+
{results}
|
|
88
|
+
|
|
89
|
+
## Conclusions
|
|
90
|
+
|
|
91
|
+
{conclusions}
|
|
92
|
+
|
|
93
|
+
## Risks and Limitations
|
|
94
|
+
|
|
95
|
+
{risks}
|
|
96
|
+
|
|
97
|
+
## Recommendations
|
|
98
|
+
|
|
99
|
+
{recommendations}
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
*Generated by Sigma Financial Intelligence*
|
|
103
|
+
""",
|
|
104
|
+
"comparison": """
|
|
105
|
+
# {title}
|
|
106
|
+
|
|
107
|
+
**Date:** {date}
|
|
108
|
+
**Author:** {author}
|
|
109
|
+
|
|
110
|
+
## Overview
|
|
111
|
+
|
|
112
|
+
{thesis}
|
|
113
|
+
|
|
114
|
+
## Comparison Summary
|
|
115
|
+
|
|
116
|
+
{key_findings}
|
|
117
|
+
|
|
118
|
+
## Detailed Analysis
|
|
119
|
+
|
|
120
|
+
{results}
|
|
121
|
+
|
|
122
|
+
## Winner Analysis
|
|
123
|
+
|
|
124
|
+
{conclusions}
|
|
125
|
+
|
|
126
|
+
## Considerations
|
|
127
|
+
|
|
128
|
+
{risks}
|
|
129
|
+
|
|
130
|
+
## Final Recommendation
|
|
131
|
+
|
|
132
|
+
{recommendations}
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
*Generated by Sigma Financial Intelligence*
|
|
136
|
+
""",
|
|
137
|
+
"backtest": """
|
|
138
|
+
# {title}
|
|
139
|
+
|
|
140
|
+
**Date:** {date}
|
|
141
|
+
**Author:** {author}
|
|
142
|
+
|
|
143
|
+
## Strategy Overview
|
|
144
|
+
|
|
145
|
+
{thesis}
|
|
146
|
+
|
|
147
|
+
## Performance Summary
|
|
148
|
+
|
|
149
|
+
{key_findings}
|
|
150
|
+
|
|
151
|
+
## Methodology
|
|
152
|
+
|
|
153
|
+
{methodology}
|
|
154
|
+
|
|
155
|
+
## Detailed Results
|
|
156
|
+
|
|
157
|
+
{results}
|
|
158
|
+
|
|
159
|
+
## Risk Analysis
|
|
160
|
+
|
|
161
|
+
{risks}
|
|
162
|
+
|
|
163
|
+
## Conclusions and Next Steps
|
|
164
|
+
|
|
165
|
+
{conclusions}
|
|
166
|
+
|
|
167
|
+
{recommendations}
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
*Generated by Sigma Financial Intelligence*
|
|
171
|
+
""",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def generate_memo(
|
|
175
|
+
self,
|
|
176
|
+
analysis_type: str,
|
|
177
|
+
title: str,
|
|
178
|
+
thesis: str,
|
|
179
|
+
key_findings: List[str],
|
|
180
|
+
methodology: str,
|
|
181
|
+
results: Dict[str, Any],
|
|
182
|
+
conclusions: List[str],
|
|
183
|
+
risks: List[str],
|
|
184
|
+
recommendations: List[str],
|
|
185
|
+
author: str = "Sigma",
|
|
186
|
+
) -> ResearchMemo:
|
|
187
|
+
"""Generate a structured research memo."""
|
|
188
|
+
|
|
189
|
+
return ResearchMemo(
|
|
190
|
+
title=title,
|
|
191
|
+
thesis=thesis,
|
|
192
|
+
key_findings=key_findings,
|
|
193
|
+
data_sources=[], # To be filled
|
|
194
|
+
methodology=methodology,
|
|
195
|
+
results=results,
|
|
196
|
+
conclusions=conclusions,
|
|
197
|
+
risks_and_limitations=risks,
|
|
198
|
+
recommendations=recommendations,
|
|
199
|
+
author=author,
|
|
200
|
+
tags=[analysis_type],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def memo_to_markdown(self, memo: ResearchMemo, template: str = "stock_analysis") -> str:
|
|
204
|
+
"""Convert memo to markdown format."""
|
|
205
|
+
|
|
206
|
+
template_str = self.MEMO_TEMPLATES.get(template, self.MEMO_TEMPLATES["stock_analysis"])
|
|
207
|
+
|
|
208
|
+
# Format lists
|
|
209
|
+
key_findings_str = "\n".join([f"- {f}" for f in memo.key_findings])
|
|
210
|
+
conclusions_str = "\n".join([f"- {c}" for c in memo.conclusions])
|
|
211
|
+
risks_str = "\n".join([f"- {r}" for r in memo.risks_and_limitations])
|
|
212
|
+
recommendations_str = "\n".join([f"- {r}" for r in memo.recommendations])
|
|
213
|
+
|
|
214
|
+
# Format results
|
|
215
|
+
results_str = self._format_results(memo.results)
|
|
216
|
+
|
|
217
|
+
return template_str.format(
|
|
218
|
+
title=memo.title,
|
|
219
|
+
date=memo.created_at.strftime("%Y-%m-%d"),
|
|
220
|
+
author=memo.author,
|
|
221
|
+
thesis=memo.thesis,
|
|
222
|
+
key_findings=key_findings_str,
|
|
223
|
+
methodology=memo.methodology,
|
|
224
|
+
results=results_str,
|
|
225
|
+
conclusions=conclusions_str,
|
|
226
|
+
risks=risks_str,
|
|
227
|
+
recommendations=recommendations_str,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _format_results(self, results: Dict[str, Any], indent: int = 0) -> str:
|
|
231
|
+
"""Format results dictionary as markdown."""
|
|
232
|
+
|
|
233
|
+
lines = []
|
|
234
|
+
prefix = " " * indent
|
|
235
|
+
|
|
236
|
+
for key, value in results.items():
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
lines.append(f"{prefix}### {key.replace('_', ' ').title()}")
|
|
239
|
+
lines.append(self._format_results(value, indent + 1))
|
|
240
|
+
elif isinstance(value, list):
|
|
241
|
+
lines.append(f"{prefix}**{key.replace('_', ' ').title()}:**")
|
|
242
|
+
for item in value:
|
|
243
|
+
lines.append(f"{prefix}- {item}")
|
|
244
|
+
elif isinstance(value, (int, float)):
|
|
245
|
+
if isinstance(value, float):
|
|
246
|
+
if abs(value) < 1:
|
|
247
|
+
formatted = f"{value:.2%}"
|
|
248
|
+
else:
|
|
249
|
+
formatted = f"{value:,.2f}"
|
|
250
|
+
else:
|
|
251
|
+
formatted = f"{value:,}"
|
|
252
|
+
lines.append(f"{prefix}**{key.replace('_', ' ').title()}:** {formatted}")
|
|
253
|
+
else:
|
|
254
|
+
lines.append(f"{prefix}**{key.replace('_', ' ').title()}:** {value}")
|
|
255
|
+
|
|
256
|
+
return "\n".join(lines)
|
|
257
|
+
|
|
258
|
+
def quick_summary(
|
|
259
|
+
self,
|
|
260
|
+
ticker: str,
|
|
261
|
+
analysis_results: Dict[str, Any],
|
|
262
|
+
) -> str:
|
|
263
|
+
"""Generate a quick 1-paragraph summary."""
|
|
264
|
+
|
|
265
|
+
# Extract key metrics
|
|
266
|
+
metrics = analysis_results.get("metrics", {})
|
|
267
|
+
|
|
268
|
+
total_return = metrics.get("total_return", 0)
|
|
269
|
+
sharpe = metrics.get("sharpe_ratio", 0)
|
|
270
|
+
max_dd = metrics.get("max_drawdown", 0)
|
|
271
|
+
|
|
272
|
+
# Generate summary
|
|
273
|
+
performance = "strong" if total_return > 0.15 else "moderate" if total_return > 0 else "weak"
|
|
274
|
+
risk_adj = "excellent" if sharpe > 1.5 else "good" if sharpe > 1 else "acceptable" if sharpe > 0.5 else "poor"
|
|
275
|
+
|
|
276
|
+
summary = (
|
|
277
|
+
f"{ticker} has shown {performance} performance with a total return of {total_return:.1%}. "
|
|
278
|
+
f"Risk-adjusted returns are {risk_adj} (Sharpe: {sharpe:.2f}). "
|
|
279
|
+
f"Maximum drawdown was {abs(max_dd):.1%}."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return summary
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ============================================================================
|
|
286
|
+
# EXPORT ENGINE
|
|
287
|
+
# ============================================================================
|
|
288
|
+
|
|
289
|
+
class ExportEngine:
|
|
290
|
+
"""Export analysis results in various formats."""
|
|
291
|
+
|
|
292
|
+
def __init__(self, output_dir: str = None):
|
|
293
|
+
self.output_dir = Path(output_dir or os.path.expanduser("~/Documents/Sigma"))
|
|
294
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
|
|
296
|
+
def export_memo(
|
|
297
|
+
self,
|
|
298
|
+
memo: ResearchMemo,
|
|
299
|
+
config: ExportConfig = None,
|
|
300
|
+
) -> str:
|
|
301
|
+
"""Export a research memo."""
|
|
302
|
+
|
|
303
|
+
config = config or ExportConfig()
|
|
304
|
+
|
|
305
|
+
# Generate filename
|
|
306
|
+
filename = f"{memo.id}_{memo.title.replace(' ', '_')[:30]}"
|
|
307
|
+
|
|
308
|
+
if config.format == "markdown":
|
|
309
|
+
return self._export_markdown(memo, filename)
|
|
310
|
+
elif config.format == "html":
|
|
311
|
+
return self._export_html(memo, filename)
|
|
312
|
+
elif config.format == "json":
|
|
313
|
+
return self._export_json(memo, filename)
|
|
314
|
+
else:
|
|
315
|
+
return self._export_markdown(memo, filename)
|
|
316
|
+
|
|
317
|
+
def _export_markdown(self, memo: ResearchMemo, filename: str) -> str:
|
|
318
|
+
"""Export as markdown."""
|
|
319
|
+
|
|
320
|
+
generator = MemoGenerator()
|
|
321
|
+
content = generator.memo_to_markdown(memo)
|
|
322
|
+
|
|
323
|
+
filepath = self.output_dir / f"{filename}.md"
|
|
324
|
+
filepath.write_text(content)
|
|
325
|
+
|
|
326
|
+
return str(filepath)
|
|
327
|
+
|
|
328
|
+
def _export_html(self, memo: ResearchMemo, filename: str) -> str:
|
|
329
|
+
"""Export as HTML."""
|
|
330
|
+
|
|
331
|
+
generator = MemoGenerator()
|
|
332
|
+
markdown_content = generator.memo_to_markdown(memo)
|
|
333
|
+
|
|
334
|
+
# Simple markdown to HTML conversion
|
|
335
|
+
html_content = self._markdown_to_html(markdown_content)
|
|
336
|
+
|
|
337
|
+
# Wrap in HTML template
|
|
338
|
+
html = f"""
|
|
339
|
+
<!DOCTYPE html>
|
|
340
|
+
<html>
|
|
341
|
+
<head>
|
|
342
|
+
<title>{memo.title}</title>
|
|
343
|
+
<style>
|
|
344
|
+
body {{
|
|
345
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
346
|
+
max-width: 800px;
|
|
347
|
+
margin: 40px auto;
|
|
348
|
+
padding: 20px;
|
|
349
|
+
line-height: 1.6;
|
|
350
|
+
color: #333;
|
|
351
|
+
}}
|
|
352
|
+
h1 {{ color: #1a1a1a; border-bottom: 2px solid #0066cc; padding-bottom: 10px; }}
|
|
353
|
+
h2 {{ color: #333; margin-top: 30px; }}
|
|
354
|
+
h3 {{ color: #555; }}
|
|
355
|
+
ul {{ padding-left: 20px; }}
|
|
356
|
+
li {{ margin: 5px 0; }}
|
|
357
|
+
strong {{ color: #1a1a1a; }}
|
|
358
|
+
.meta {{ color: #666; font-size: 0.9em; margin-bottom: 20px; }}
|
|
359
|
+
</style>
|
|
360
|
+
</head>
|
|
361
|
+
<body>
|
|
362
|
+
{html_content}
|
|
363
|
+
</body>
|
|
364
|
+
</html>
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
filepath = self.output_dir / f"{filename}.html"
|
|
368
|
+
filepath.write_text(html)
|
|
369
|
+
|
|
370
|
+
return str(filepath)
|
|
371
|
+
|
|
372
|
+
def _export_json(self, memo: ResearchMemo, filename: str) -> str:
|
|
373
|
+
"""Export as JSON."""
|
|
374
|
+
|
|
375
|
+
filepath = self.output_dir / f"{filename}.json"
|
|
376
|
+
filepath.write_text(memo.model_dump_json(indent=2))
|
|
377
|
+
|
|
378
|
+
return str(filepath)
|
|
379
|
+
|
|
380
|
+
def _markdown_to_html(self, markdown: str) -> str:
|
|
381
|
+
"""Simple markdown to HTML conversion."""
|
|
382
|
+
|
|
383
|
+
lines = markdown.split("\n")
|
|
384
|
+
html_lines = []
|
|
385
|
+
in_list = False
|
|
386
|
+
|
|
387
|
+
for line in lines:
|
|
388
|
+
# Headers
|
|
389
|
+
if line.startswith("# "):
|
|
390
|
+
html_lines.append(f"<h1>{line[2:]}</h1>")
|
|
391
|
+
elif line.startswith("## "):
|
|
392
|
+
html_lines.append(f"<h2>{line[3:]}</h2>")
|
|
393
|
+
elif line.startswith("### "):
|
|
394
|
+
html_lines.append(f"<h3>{line[4:]}</h3>")
|
|
395
|
+
# Lists
|
|
396
|
+
elif line.startswith("- "):
|
|
397
|
+
if not in_list:
|
|
398
|
+
html_lines.append("<ul>")
|
|
399
|
+
in_list = True
|
|
400
|
+
html_lines.append(f"<li>{line[2:]}</li>")
|
|
401
|
+
# Bold
|
|
402
|
+
elif line.startswith("**") and ":**" in line:
|
|
403
|
+
if in_list:
|
|
404
|
+
html_lines.append("</ul>")
|
|
405
|
+
in_list = False
|
|
406
|
+
parts = line.split(":**", 1)
|
|
407
|
+
key = parts[0].replace("**", "")
|
|
408
|
+
value = parts[1] if len(parts) > 1 else ""
|
|
409
|
+
html_lines.append(f"<p><strong>{key}:</strong>{value}</p>")
|
|
410
|
+
# Empty line
|
|
411
|
+
elif not line.strip():
|
|
412
|
+
if in_list:
|
|
413
|
+
html_lines.append("</ul>")
|
|
414
|
+
in_list = False
|
|
415
|
+
html_lines.append("<br>")
|
|
416
|
+
# Regular text
|
|
417
|
+
else:
|
|
418
|
+
if in_list:
|
|
419
|
+
html_lines.append("</ul>")
|
|
420
|
+
in_list = False
|
|
421
|
+
html_lines.append(f"<p>{line}</p>")
|
|
422
|
+
|
|
423
|
+
if in_list:
|
|
424
|
+
html_lines.append("</ul>")
|
|
425
|
+
|
|
426
|
+
return "\n".join(html_lines)
|
|
427
|
+
|
|
428
|
+
def export_data(
|
|
429
|
+
self,
|
|
430
|
+
data: pd.DataFrame,
|
|
431
|
+
filename: str,
|
|
432
|
+
format: str = "csv",
|
|
433
|
+
) -> str:
|
|
434
|
+
"""Export DataFrame to file."""
|
|
435
|
+
|
|
436
|
+
if format == "csv":
|
|
437
|
+
filepath = self.output_dir / f"{filename}.csv"
|
|
438
|
+
data.to_csv(filepath)
|
|
439
|
+
elif format == "excel":
|
|
440
|
+
filepath = self.output_dir / f"{filename}.xlsx"
|
|
441
|
+
data.to_excel(filepath)
|
|
442
|
+
elif format == "parquet":
|
|
443
|
+
filepath = self.output_dir / f"{filename}.parquet"
|
|
444
|
+
data.to_parquet(filepath)
|
|
445
|
+
else:
|
|
446
|
+
filepath = self.output_dir / f"{filename}.csv"
|
|
447
|
+
data.to_csv(filepath)
|
|
448
|
+
|
|
449
|
+
return str(filepath)
|
|
450
|
+
|
|
451
|
+
def export_chart(
|
|
452
|
+
self,
|
|
453
|
+
fig, # plotly figure
|
|
454
|
+
filename: str,
|
|
455
|
+
format: str = "png",
|
|
456
|
+
scale: int = 2,
|
|
457
|
+
) -> str:
|
|
458
|
+
"""Export chart to file."""
|
|
459
|
+
|
|
460
|
+
filepath = self.output_dir / f"{filename}.{format}"
|
|
461
|
+
|
|
462
|
+
if format == "html":
|
|
463
|
+
fig.write_html(str(filepath))
|
|
464
|
+
else:
|
|
465
|
+
fig.write_image(str(filepath), format=format, scale=scale)
|
|
466
|
+
|
|
467
|
+
return str(filepath)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ============================================================================
|
|
471
|
+
# REPRODUCIBILITY ENGINE
|
|
472
|
+
# ============================================================================
|
|
473
|
+
|
|
474
|
+
class ReproducibilityEngine:
|
|
475
|
+
"""Ensure reproducibility of analysis."""
|
|
476
|
+
|
|
477
|
+
def __init__(self, config_dir: str = None):
|
|
478
|
+
self.config_dir = Path(config_dir or os.path.expanduser("~/.sigma/reproducibility"))
|
|
479
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
480
|
+
|
|
481
|
+
def save_config(
|
|
482
|
+
self,
|
|
483
|
+
query: str,
|
|
484
|
+
parameters: Dict[str, Any],
|
|
485
|
+
data_sources: List[str],
|
|
486
|
+
random_seed: Optional[int] = None,
|
|
487
|
+
) -> str:
|
|
488
|
+
"""Save reproducibility configuration."""
|
|
489
|
+
|
|
490
|
+
import hashlib
|
|
491
|
+
|
|
492
|
+
config = ReproducibilityConfig(
|
|
493
|
+
query=query,
|
|
494
|
+
parameters=parameters,
|
|
495
|
+
data_sources=data_sources,
|
|
496
|
+
random_seed=random_seed,
|
|
497
|
+
versions=self._get_versions(),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Generate config ID
|
|
501
|
+
config_hash = hashlib.sha256(
|
|
502
|
+
f"{query}{json.dumps(parameters, sort_keys=True)}".encode()
|
|
503
|
+
).hexdigest()[:12]
|
|
504
|
+
|
|
505
|
+
config_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{config_hash}"
|
|
506
|
+
|
|
507
|
+
# Save config
|
|
508
|
+
filepath = self.config_dir / f"{config_id}.json"
|
|
509
|
+
filepath.write_text(config.model_dump_json(indent=2))
|
|
510
|
+
|
|
511
|
+
return config_id
|
|
512
|
+
|
|
513
|
+
def load_config(self, config_id: str) -> Optional[ReproducibilityConfig]:
|
|
514
|
+
"""Load reproducibility configuration."""
|
|
515
|
+
|
|
516
|
+
filepath = self.config_dir / f"{config_id}.json"
|
|
517
|
+
|
|
518
|
+
if not filepath.exists():
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
data = json.loads(filepath.read_text())
|
|
522
|
+
return ReproducibilityConfig(**data)
|
|
523
|
+
|
|
524
|
+
def _get_versions(self) -> Dict[str, str]:
|
|
525
|
+
"""Get versions of key packages."""
|
|
526
|
+
|
|
527
|
+
versions = {}
|
|
528
|
+
|
|
529
|
+
packages = ["pandas", "numpy", "scipy", "plotly", "yfinance"]
|
|
530
|
+
|
|
531
|
+
for package in packages:
|
|
532
|
+
try:
|
|
533
|
+
import importlib
|
|
534
|
+
mod = importlib.import_module(package)
|
|
535
|
+
versions[package] = getattr(mod, "__version__", "unknown")
|
|
536
|
+
except ImportError:
|
|
537
|
+
versions[package] = "not installed"
|
|
538
|
+
|
|
539
|
+
return versions
|
|
540
|
+
|
|
541
|
+
def generate_reproduction_script(
|
|
542
|
+
self,
|
|
543
|
+
config: ReproducibilityConfig,
|
|
544
|
+
) -> str:
|
|
545
|
+
"""Generate a Python script to reproduce the analysis."""
|
|
546
|
+
|
|
547
|
+
script = f'''#!/usr/bin/env python3
|
|
548
|
+
"""
|
|
549
|
+
Reproduction Script
|
|
550
|
+
Generated: {datetime.now().isoformat()}
|
|
551
|
+
Original Query: {config.query}
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
import pandas as pd
|
|
555
|
+
import numpy as np
|
|
556
|
+
|
|
557
|
+
# Set random seed for reproducibility
|
|
558
|
+
{f"np.random.seed({config.random_seed})" if config.random_seed else "# No random seed specified"}
|
|
559
|
+
|
|
560
|
+
# Parameters
|
|
561
|
+
PARAMETERS = {json.dumps(config.parameters, indent=4)}
|
|
562
|
+
|
|
563
|
+
# Data sources
|
|
564
|
+
DATA_SOURCES = {json.dumps(config.data_sources, indent=4)}
|
|
565
|
+
|
|
566
|
+
# Expected versions (for verification)
|
|
567
|
+
EXPECTED_VERSIONS = {json.dumps(config.versions, indent=4)}
|
|
568
|
+
|
|
569
|
+
def verify_versions():
|
|
570
|
+
"""Verify package versions match original analysis."""
|
|
571
|
+
import warnings
|
|
572
|
+
for package, expected in EXPECTED_VERSIONS.items():
|
|
573
|
+
try:
|
|
574
|
+
import importlib
|
|
575
|
+
mod = importlib.import_module(package)
|
|
576
|
+
actual = getattr(mod, "__version__", "unknown")
|
|
577
|
+
if actual != expected:
|
|
578
|
+
warnings.warn(f"{{package}} version mismatch: {{actual}} vs {{expected}}")
|
|
579
|
+
except ImportError:
|
|
580
|
+
warnings.warn(f"{{package}} not installed")
|
|
581
|
+
|
|
582
|
+
def main():
|
|
583
|
+
"""Reproduce the analysis."""
|
|
584
|
+
verify_versions()
|
|
585
|
+
|
|
586
|
+
# TODO: Add reproduction code
|
|
587
|
+
print("Reproduction script generated. Add your analysis code here.")
|
|
588
|
+
print(f"Original query: {config.query}")
|
|
589
|
+
|
|
590
|
+
if __name__ == "__main__":
|
|
591
|
+
main()
|
|
592
|
+
'''
|
|
593
|
+
|
|
594
|
+
return script
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ============================================================================
|
|
598
|
+
# SESSION LOGGER
|
|
599
|
+
# ============================================================================
|
|
600
|
+
|
|
601
|
+
class SessionLogger:
|
|
602
|
+
"""Log analysis sessions for audit and reproducibility."""
|
|
603
|
+
|
|
604
|
+
def __init__(self, log_dir: str = None):
|
|
605
|
+
self.log_dir = Path(log_dir or os.path.expanduser("~/.sigma/logs"))
|
|
606
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
607
|
+
self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
608
|
+
self.entries = []
|
|
609
|
+
|
|
610
|
+
def log_query(self, query: str, response: str = None):
|
|
611
|
+
"""Log a query and response."""
|
|
612
|
+
|
|
613
|
+
entry = {
|
|
614
|
+
"timestamp": datetime.now().isoformat(),
|
|
615
|
+
"type": "query",
|
|
616
|
+
"query": query,
|
|
617
|
+
"response": response,
|
|
618
|
+
}
|
|
619
|
+
self.entries.append(entry)
|
|
620
|
+
|
|
621
|
+
def log_action(self, action: str, details: Dict[str, Any] = None):
|
|
622
|
+
"""Log an action."""
|
|
623
|
+
|
|
624
|
+
entry = {
|
|
625
|
+
"timestamp": datetime.now().isoformat(),
|
|
626
|
+
"type": "action",
|
|
627
|
+
"action": action,
|
|
628
|
+
"details": details or {},
|
|
629
|
+
}
|
|
630
|
+
self.entries.append(entry)
|
|
631
|
+
|
|
632
|
+
def log_error(self, error: str, context: Dict[str, Any] = None):
|
|
633
|
+
"""Log an error."""
|
|
634
|
+
|
|
635
|
+
entry = {
|
|
636
|
+
"timestamp": datetime.now().isoformat(),
|
|
637
|
+
"type": "error",
|
|
638
|
+
"error": error,
|
|
639
|
+
"context": context or {},
|
|
640
|
+
}
|
|
641
|
+
self.entries.append(entry)
|
|
642
|
+
|
|
643
|
+
def save_session(self) -> str:
|
|
644
|
+
"""Save session log to file."""
|
|
645
|
+
|
|
646
|
+
filepath = self.log_dir / f"session_{self.session_id}.json"
|
|
647
|
+
|
|
648
|
+
session_data = {
|
|
649
|
+
"session_id": self.session_id,
|
|
650
|
+
"start_time": self.entries[0]["timestamp"] if self.entries else None,
|
|
651
|
+
"end_time": datetime.now().isoformat(),
|
|
652
|
+
"entry_count": len(self.entries),
|
|
653
|
+
"entries": self.entries,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
filepath.write_text(json.dumps(session_data, indent=2))
|
|
657
|
+
|
|
658
|
+
return str(filepath)
|