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/__init__.py +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +801 -892
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +465 -0
- sigma/comparison.py +611 -0
- sigma/config.py +366 -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 +639 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +374 -403
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.3.0.dist-info/METADATA +583 -0
- sigma_terminal-3.3.0.dist-info/RECORD +30 -0
- sigma_terminal-3.3.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.3.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.2.dist-info/METADATA +0 -222
- sigma_terminal-2.0.2.dist-info/RECORD +0 -19
- sigma_terminal-2.0.2.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.2.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.2.dist-info → sigma_terminal-3.3.0.dist-info}/WHEEL +0 -0
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
|
+
}
|