sigma-terminal 2.0.2__py3-none-any.whl → 3.3.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/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)