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/core/engine.py ADDED
@@ -0,0 +1,493 @@
1
+ """Main research engine orchestrating all Sigma capabilities."""
2
+
3
+ import asyncio
4
+ import json
5
+ from datetime import datetime, date
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ from .models import (
9
+ ResearchPlan,
10
+ DeliverableType,
11
+ DataLineage,
12
+ DataQualityReport,
13
+ PerformanceMetrics,
14
+ ComparisonResult,
15
+ BacktestResult,
16
+ ResearchMemo,
17
+ Alert,
18
+ RegimeAnalysis,
19
+ Regime,
20
+ )
21
+ from .intent import IntentParser, DecisivenessEngine, PromptPresets
22
+
23
+
24
+ class SigmaEngine:
25
+ """Main research engine for Sigma."""
26
+
27
+ def __init__(self):
28
+ self.intent_parser = IntentParser()
29
+ self.decisiveness = DecisivenessEngine()
30
+ self.presets = PromptPresets()
31
+ self.data_cache = {}
32
+ self.lineage_tracker = []
33
+
34
+ async def process_query(self, query: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
35
+ """Process a user query end-to-end."""
36
+ # Parse intent
37
+ plan = self.intent_parser.parse(query)
38
+
39
+ # Check if clarifications needed
40
+ if plan.clarifications_needed:
41
+ return {
42
+ "type": "clarification",
43
+ "questions": plan.clarifications_needed,
44
+ "partial_plan": plan.model_dump(),
45
+ }
46
+
47
+ # Handle vague queries with decisiveness engine
48
+ vague_translation = self.decisiveness.translate_vague_query(query)
49
+ if vague_translation["criteria"]:
50
+ plan.context["measurable_criteria"] = vague_translation
51
+
52
+ # Route to appropriate handler
53
+ result = await self._route_deliverable(plan)
54
+
55
+ return {
56
+ "type": "result",
57
+ "plan": plan.model_dump(),
58
+ "result": result,
59
+ }
60
+
61
+ async def _route_deliverable(self, plan: ResearchPlan) -> Dict[str, Any]:
62
+ """Route to appropriate handler based on deliverable type."""
63
+ handlers = {
64
+ DeliverableType.ANALYSIS: self._handle_analysis,
65
+ DeliverableType.COMPARISON: self._handle_comparison,
66
+ DeliverableType.BACKTEST: self._handle_backtest,
67
+ DeliverableType.PORTFOLIO: self._handle_portfolio,
68
+ DeliverableType.STRATEGY: self._handle_strategy,
69
+ DeliverableType.CHART: self._handle_chart,
70
+ DeliverableType.REPORT: self._handle_report,
71
+ DeliverableType.ALERT: self._handle_alert,
72
+ }
73
+
74
+ handler = handlers.get(plan.deliverable, self._handle_analysis)
75
+ return await handler(plan)
76
+
77
+ async def _handle_analysis(self, plan: ResearchPlan) -> Dict[str, Any]:
78
+ """Handle general analysis request."""
79
+ results = {}
80
+
81
+ for symbol in plan.assets:
82
+ # This will be implemented with actual data fetching
83
+ results[symbol] = {
84
+ "symbol": symbol,
85
+ "analysis_type": "comprehensive",
86
+ "metrics": {},
87
+ "insights": [],
88
+ }
89
+
90
+ return {"analyses": results}
91
+
92
+ async def _handle_comparison(self, plan: ResearchPlan) -> Dict[str, Any]:
93
+ """Handle comparison request."""
94
+ # Get measurable criteria from vague query
95
+ criteria = plan.context.get("measurable_criteria", {})
96
+
97
+ return {
98
+ "comparison_type": "multi_asset",
99
+ "assets": plan.assets,
100
+ "criteria": criteria.get("criteria", []),
101
+ "interpretation": criteria.get("interpretation", ""),
102
+ }
103
+
104
+ async def _handle_backtest(self, plan: ResearchPlan) -> Dict[str, Any]:
105
+ """Handle backtest request."""
106
+ return {
107
+ "backtest_type": "strategy",
108
+ "assets": plan.assets,
109
+ "period": plan.lookback_period,
110
+ "constraints": [c.model_dump() for c in plan.constraints],
111
+ }
112
+
113
+ async def _handle_portfolio(self, plan: ResearchPlan) -> Dict[str, Any]:
114
+ """Handle portfolio construction request."""
115
+ return {
116
+ "portfolio_type": "optimization",
117
+ "assets": plan.assets,
118
+ "risk_profile": plan.risk_profile,
119
+ "constraints": [c.model_dump() for c in plan.constraints],
120
+ }
121
+
122
+ async def _handle_strategy(self, plan: ResearchPlan) -> Dict[str, Any]:
123
+ """Handle strategy discovery request."""
124
+ return {
125
+ "strategy_type": "discovery",
126
+ "assets": plan.assets,
127
+ "horizon": plan.horizon,
128
+ }
129
+
130
+ async def _handle_chart(self, plan: ResearchPlan) -> Dict[str, Any]:
131
+ """Handle chart generation request."""
132
+ return {
133
+ "chart_type": "price",
134
+ "assets": plan.assets,
135
+ "period": plan.lookback_period,
136
+ }
137
+
138
+ async def _handle_report(self, plan: ResearchPlan) -> Dict[str, Any]:
139
+ """Handle report generation request."""
140
+ return {
141
+ "report_type": "research_memo",
142
+ "assets": plan.assets,
143
+ }
144
+
145
+ async def _handle_alert(self, plan: ResearchPlan) -> Dict[str, Any]:
146
+ """Handle alert setup request."""
147
+ return {
148
+ "alert_type": "watchlist",
149
+ "assets": plan.assets,
150
+ }
151
+
152
+ # ========================================================================
153
+ # UTILITY METHODS
154
+ # ========================================================================
155
+
156
+ def get_presets(self) -> List[Dict[str, str]]:
157
+ """Get available prompt presets."""
158
+ return self.presets.list_presets()
159
+
160
+ def apply_preset(self, preset_name: str, **kwargs) -> Optional[str]:
161
+ """Apply a prompt preset."""
162
+ return self.presets.get_preset(preset_name, **kwargs)
163
+
164
+ def get_show_work_mode(self) -> bool:
165
+ """Check if show work mode is enabled."""
166
+ return getattr(self, "_show_work", False)
167
+
168
+ def set_show_work_mode(self, enabled: bool):
169
+ """Enable/disable show work mode."""
170
+ self._show_work = enabled
171
+
172
+ def explain_technical(self, concept: str) -> str:
173
+ """Explain a concept with formulas and definitions."""
174
+ explanations = {
175
+ "sharpe_ratio": """
176
+ **Sharpe Ratio**
177
+ Formula: (Rp - Rf) / σp
178
+ Where:
179
+ - Rp = Portfolio return
180
+ - Rf = Risk-free rate
181
+ - σp = Portfolio standard deviation
182
+
183
+ Interpretation: Risk-adjusted return per unit of volatility. Higher is better.
184
+ Typical values: <1 = poor, 1-2 = good, >2 = excellent
185
+ """,
186
+ "sortino_ratio": """
187
+ **Sortino Ratio**
188
+ Formula: (Rp - Rf) / σd
189
+ Where:
190
+ - Rp = Portfolio return
191
+ - Rf = Risk-free rate (or target return)
192
+ - σd = Downside deviation (only negative returns)
193
+
194
+ Interpretation: Like Sharpe but only penalizes downside volatility.
195
+ Better for asymmetric return distributions.
196
+ """,
197
+ "max_drawdown": """
198
+ **Maximum Drawdown**
199
+ Formula: (Peak - Trough) / Peak
200
+ Measures the largest peak-to-trough decline.
201
+
202
+ Interpretation: Worst-case loss from a peak.
203
+ Context: A 50% drawdown requires 100% gain to recover.
204
+ """,
205
+ "beta": """
206
+ **Beta (β)**
207
+ Formula: Cov(Ri, Rm) / Var(Rm)
208
+ Where:
209
+ - Ri = Asset return
210
+ - Rm = Market return
211
+
212
+ Interpretation: Sensitivity to market movements.
213
+ β = 1: Moves with market
214
+ β > 1: More volatile than market
215
+ β < 1: Less volatile than market
216
+ """,
217
+ "var": """
218
+ **Value at Risk (VaR)**
219
+ Formula: Quantile of return distribution at confidence level
220
+ Example: 95% VaR = 5th percentile of returns
221
+
222
+ Interpretation: Maximum expected loss at given confidence level.
223
+ 95% VaR of -3% means 95% of the time, loss won't exceed 3%.
224
+ """,
225
+ "cvar": """
226
+ **Conditional VaR (CVaR) / Expected Shortfall**
227
+ Formula: E[Loss | Loss > VaR]
228
+ Average loss in the worst cases beyond VaR.
229
+
230
+ Interpretation: Expected loss when VaR is breached.
231
+ Better captures tail risk than VaR alone.
232
+ """,
233
+ }
234
+
235
+ return explanations.get(concept.lower(), f"No detailed explanation available for: {concept}")
236
+
237
+
238
+ # ============================================================================
239
+ # AUTOCOMPLETE ENGINE
240
+ # ============================================================================
241
+
242
+ class AutocompleteEngine:
243
+ """Provide intelligent autocomplete suggestions."""
244
+
245
+ # Common commands
246
+ COMMANDS = [
247
+ "/help", "/keys", "/models", "/provider", "/model", "/backtest",
248
+ "/status", "/export", "/clear", "/compare", "/chart", "/report",
249
+ "/alert", "/watchlist", "/portfolio", "/strategy", "/preset",
250
+ ]
251
+
252
+ # Common phrases
253
+ PHRASES = [
254
+ "analyze {ticker}",
255
+ "compare {ticker1} vs {ticker2}",
256
+ "backtest {strategy} on {ticker}",
257
+ "show me a chart of {ticker}",
258
+ "what's the sentiment on {ticker}",
259
+ "build a portfolio with {tickers}",
260
+ "run technical analysis on {ticker}",
261
+ "how does {ticker} compare to {benchmark}",
262
+ "what's the Sharpe ratio of {ticker}",
263
+ "show factor exposures for {ticker}",
264
+ "detect regime for {ticker}",
265
+ "run stress test on {portfolio}",
266
+ "generate research memo for {ticker}",
267
+ "set alert when {ticker} drops below {price}",
268
+ ]
269
+
270
+ # Strategy names
271
+ STRATEGIES = [
272
+ "sma_crossover", "rsi_mean_reversion", "macd_momentum",
273
+ "bollinger_bands", "dual_momentum", "breakout",
274
+ "trend_following", "mean_reversion", "carry",
275
+ "value", "quality", "momentum", "low_volatility",
276
+ ]
277
+
278
+ # Common tickers
279
+ TICKERS = [
280
+ "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA",
281
+ "SPY", "QQQ", "IWM", "DIA", "VTI", "VOO",
282
+ "XLK", "XLF", "XLE", "XLV", "XLI",
283
+ "GLD", "SLV", "TLT", "BND",
284
+ "BTC", "ETH",
285
+ ]
286
+
287
+ @classmethod
288
+ def get_suggestions(cls, text: str, max_results: int = 10) -> List[str]:
289
+ """Get autocomplete suggestions for partial input."""
290
+ text = text.lower().strip()
291
+ suggestions = []
292
+
293
+ # Command completion
294
+ if text.startswith("/"):
295
+ suggestions.extend([
296
+ cmd for cmd in cls.COMMANDS
297
+ if cmd.lower().startswith(text)
298
+ ])
299
+
300
+ # Ticker completion
301
+ words = text.split()
302
+ if words:
303
+ last_word = words[-1].upper()
304
+ if len(last_word) >= 1:
305
+ matching_tickers = [
306
+ t for t in cls.TICKERS
307
+ if t.startswith(last_word)
308
+ ]
309
+ suggestions.extend([
310
+ " ".join(words[:-1] + [t]) for t in matching_tickers
311
+ ])
312
+
313
+ # Strategy completion
314
+ if "backtest" in text or "strategy" in text:
315
+ for strategy in cls.STRATEGIES:
316
+ if strategy not in text:
317
+ suggestions.append(text + " " + strategy)
318
+
319
+ # Phrase completion
320
+ for phrase in cls.PHRASES:
321
+ phrase_lower = phrase.lower()
322
+ if text in phrase_lower:
323
+ suggestions.append(phrase)
324
+
325
+ return suggestions[:max_results]
326
+
327
+ @classmethod
328
+ def get_ticker_suggestions(cls, partial: str) -> List[str]:
329
+ """Get ticker suggestions for partial input."""
330
+ partial = partial.upper()
331
+ return [t for t in cls.TICKERS if t.startswith(partial)][:10]
332
+
333
+ @classmethod
334
+ def get_command_help(cls, command: str) -> str:
335
+ """Get help text for a command."""
336
+ help_texts = {
337
+ "/help": "Show all available commands",
338
+ "/keys": "Configure API keys for providers",
339
+ "/models": "List available AI models",
340
+ "/provider": "Switch AI provider (google, openai, anthropic, groq, ollama)",
341
+ "/model": "Switch to a specific model",
342
+ "/backtest": "Show available backtest strategies",
343
+ "/status": "Show current configuration",
344
+ "/export": "Export conversation to file",
345
+ "/clear": "Clear chat history",
346
+ "/compare": "Compare multiple assets",
347
+ "/chart": "Generate a chart",
348
+ "/report": "Generate a research report",
349
+ "/alert": "Set up price or signal alerts",
350
+ "/watchlist": "Manage your watchlist",
351
+ "/portfolio": "Portfolio analysis and optimization",
352
+ "/strategy": "Discover and test strategies",
353
+ "/preset": "Use a prompt preset template",
354
+ }
355
+ return help_texts.get(command, "No help available for this command")
356
+
357
+
358
+ # ============================================================================
359
+ # SHOW WORK MODE
360
+ # ============================================================================
361
+
362
+ class ShowWorkLogger:
363
+ """Log and display the agent's reasoning process."""
364
+
365
+ def __init__(self):
366
+ self.steps = []
367
+ self.assumptions = []
368
+ self.scoring_rubric = {}
369
+
370
+ def log_step(self, step: str, details: Optional[Dict[str, Any]] = None):
371
+ """Log a reasoning step."""
372
+ self.steps.append({
373
+ "timestamp": datetime.now().isoformat(),
374
+ "step": step,
375
+ "details": details or {},
376
+ })
377
+
378
+ def log_assumption(self, assumption: str):
379
+ """Log an assumption being made."""
380
+ self.assumptions.append(assumption)
381
+
382
+ def set_scoring_rubric(self, rubric: Dict[str, float]):
383
+ """Set the scoring rubric being used."""
384
+ self.scoring_rubric = rubric
385
+
386
+ def get_work_log(self) -> str:
387
+ """Get formatted work log."""
388
+ lines = []
389
+ lines.append("## Reasoning Process\n")
390
+
391
+ if self.assumptions:
392
+ lines.append("### Assumptions")
393
+ for a in self.assumptions:
394
+ lines.append(f"- {a}")
395
+ lines.append("")
396
+
397
+ if self.scoring_rubric:
398
+ lines.append("### Scoring Rubric")
399
+ for criterion, weight in self.scoring_rubric.items():
400
+ lines.append(f"- {criterion}: {weight:.1%}")
401
+ lines.append("")
402
+
403
+ if self.steps:
404
+ lines.append("### Steps Taken")
405
+ for i, step in enumerate(self.steps, 1):
406
+ lines.append(f"{i}. {step['step']}")
407
+ if step.get("details"):
408
+ for k, v in step["details"].items():
409
+ lines.append(f" - {k}: {v}")
410
+ lines.append("")
411
+
412
+ return "\n".join(lines)
413
+
414
+ def clear(self):
415
+ """Clear the work log."""
416
+ self.steps = []
417
+ self.assumptions = []
418
+ self.scoring_rubric = {}
419
+
420
+
421
+ # ============================================================================
422
+ # SAFETY GUARDRAILS
423
+ # ============================================================================
424
+
425
+ class SafetyGuardrails:
426
+ """Enforce safety and correctness checks."""
427
+
428
+ @staticmethod
429
+ def check_lookahead_bias(code: str) -> List[str]:
430
+ """Check for potential lookahead bias in code."""
431
+ warnings = []
432
+
433
+ # Common lookahead patterns
434
+ patterns = [
435
+ (r"shift\(-", "Negative shift may cause lookahead bias"),
436
+ (r"\.future", "Future reference detected"),
437
+ (r"iloc\[-\d+\]", "Negative indexing without proper offset"),
438
+ (r"fillna\(method='bfill'\)", "Backward fill can cause lookahead"),
439
+ ]
440
+
441
+ import re
442
+ for pattern, message in patterns:
443
+ if re.search(pattern, code):
444
+ warnings.append(message)
445
+
446
+ return warnings
447
+
448
+ @staticmethod
449
+ def check_sample_size(n_samples: int, n_parameters: int) -> Dict[str, Any]:
450
+ """Check if sample size is sufficient."""
451
+ min_recommended = n_parameters * 50 # Rule of thumb
452
+
453
+ return {
454
+ "sample_size": n_samples,
455
+ "parameters": n_parameters,
456
+ "min_recommended": min_recommended,
457
+ "sufficient": n_samples >= min_recommended,
458
+ "warning": f"Sample size ({n_samples}) may be too small for {n_parameters} parameters. Recommend at least {min_recommended}." if n_samples < min_recommended else None,
459
+ }
460
+
461
+ @staticmethod
462
+ def validate_indicator_timing(indicator_name: str, window: int, data_length: int) -> Dict[str, Any]:
463
+ """Validate that indicator uses only past data."""
464
+ warmup_needed = window
465
+ valid_start = warmup_needed
466
+
467
+ return {
468
+ "indicator": indicator_name,
469
+ "window": window,
470
+ "data_length": data_length,
471
+ "warmup_needed": warmup_needed,
472
+ "valid_start_index": valid_start,
473
+ "warning": f"First {warmup_needed} observations are warmup period" if warmup_needed > 0 else None,
474
+ }
475
+
476
+ @staticmethod
477
+ def disclaimer() -> str:
478
+ """Get standard disclaimer."""
479
+ return """
480
+ **Disclaimer**: This analysis is for informational and educational purposes only.
481
+ It does not constitute financial advice, investment recommendations, or a solicitation
482
+ to buy or sell securities. Past performance does not guarantee future results.
483
+ Always consult with a qualified financial advisor before making investment decisions.
484
+ """.strip()
485
+
486
+ @staticmethod
487
+ def separate_research_from_advice(content: str) -> Dict[str, str]:
488
+ """Explicitly separate research findings from advice."""
489
+ return {
490
+ "research_findings": content,
491
+ "advice_section": "For personalized advice, please consult a licensed financial advisor.",
492
+ "disclaimer": SafetyGuardrails.disclaimer(),
493
+ }