sigma-terminal 2.0.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 +9 -0
- sigma/__main__.py +6 -0
- sigma/app.py +947 -0
- sigma/core/__init__.py +18 -0
- sigma/core/agent.py +205 -0
- sigma/core/config.py +119 -0
- sigma/core/llm.py +794 -0
- sigma/core/models.py +153 -0
- sigma/setup.py +455 -0
- sigma/tools/__init__.py +5 -0
- sigma/tools/backtest.py +1506 -0
- sigma/tools/charts.py +400 -0
- sigma/tools/financial.py +1457 -0
- sigma/ui/__init__.py +1 -0
- sigma_terminal-2.0.0.dist-info/METADATA +222 -0
- sigma_terminal-2.0.0.dist-info/RECORD +19 -0
- sigma_terminal-2.0.0.dist-info/WHEEL +4 -0
- sigma_terminal-2.0.0.dist-info/entry_points.txt +2 -0
- sigma_terminal-2.0.0.dist-info/licenses/LICENSE +42 -0
sigma/app.py
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
"""Sigma CLI - Professional financial research interface."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.padding import Padding
|
|
15
|
+
from rich.box import ROUNDED, SIMPLE, HEAVY, DOUBLE
|
|
16
|
+
from rich.columns import Columns
|
|
17
|
+
from rich import box
|
|
18
|
+
|
|
19
|
+
from sigma.core.agent import SigmaAgent
|
|
20
|
+
from sigma.core.config import LLMProvider, get_settings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
# Version
|
|
26
|
+
VERSION = "2.0.0"
|
|
27
|
+
|
|
28
|
+
# ASCII Art Banner
|
|
29
|
+
BANNER = """[bold bright_blue]
|
|
30
|
+
███████╗██╗ ██████╗ ███╗ ███╗ █████╗
|
|
31
|
+
██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
|
|
32
|
+
███████╗██║██║ ███╗██╔████╔██║███████║
|
|
33
|
+
╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
|
|
34
|
+
███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
|
|
35
|
+
╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold bright_blue]
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
SUB_BANNER = """[dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/dim]
|
|
39
|
+
[bold white] Institutional-Grade Financial Research Agent[/bold white]
|
|
40
|
+
[dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/dim]"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_banner(model: str):
|
|
44
|
+
"""Print the startup banner."""
|
|
45
|
+
console.print()
|
|
46
|
+
console.print(BANNER)
|
|
47
|
+
console.print(SUB_BANNER)
|
|
48
|
+
console.print()
|
|
49
|
+
|
|
50
|
+
# Status line
|
|
51
|
+
console.print(f" [dim]Version:[/dim] [bright_cyan]{VERSION}[/bright_cyan] [dim]Model:[/dim] [bright_cyan]{model}[/bright_cyan]")
|
|
52
|
+
console.print()
|
|
53
|
+
console.print(" [dim]Type[/dim] [bold bright_yellow]/help[/bold bright_yellow] [dim]for commands[/dim] [dim]Type[/dim] [bold bright_yellow]/quit[/bold bright_yellow] [dim]to exit[/dim]")
|
|
54
|
+
console.print()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_help():
|
|
58
|
+
"""Print comprehensive help."""
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
# Commands table
|
|
62
|
+
commands = Table(title="[bold bright_cyan]Commands[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
63
|
+
commands.add_column("Command", style="bright_yellow")
|
|
64
|
+
commands.add_column("Description", style="white")
|
|
65
|
+
|
|
66
|
+
commands.add_row("/help, /h, /?", "Show this help message")
|
|
67
|
+
commands.add_row("/model <provider>", "Switch LLM provider (openai, anthropic, google, groq, ollama)")
|
|
68
|
+
commands.add_row("/mode <mode>", "Switch analysis mode (default, technical, fundamental, quant)")
|
|
69
|
+
commands.add_row("/clear", "Clear conversation history")
|
|
70
|
+
commands.add_row("/status", "Show current configuration")
|
|
71
|
+
commands.add_row("/chart <symbol>", "Quick price chart for a symbol")
|
|
72
|
+
commands.add_row("/compare <sym1> <sym2>...", "Quick comparison chart")
|
|
73
|
+
commands.add_row("/backtest <symbol> <strategy>", "Generate backtest algorithm")
|
|
74
|
+
commands.add_row("/lean setup", "Setup LEAN Engine for backtesting")
|
|
75
|
+
commands.add_row("/lean run <symbol> <strategy>", "Run backtest with LEAN Engine")
|
|
76
|
+
commands.add_row("/quit, /exit, /q", "Exit Sigma")
|
|
77
|
+
|
|
78
|
+
console.print(commands)
|
|
79
|
+
console.print()
|
|
80
|
+
|
|
81
|
+
# Analysis modes
|
|
82
|
+
modes = Table(title="[bold bright_cyan]Analysis Modes[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
83
|
+
modes.add_column("Mode", style="bright_yellow")
|
|
84
|
+
modes.add_column("Description", style="white")
|
|
85
|
+
|
|
86
|
+
modes.add_row("default", "Comprehensive analysis using all available tools")
|
|
87
|
+
modes.add_row("technical", "Focus on technical indicators, charts, and price action")
|
|
88
|
+
modes.add_row("fundamental", "Focus on financial statements, ratios, and valuations")
|
|
89
|
+
modes.add_row("quant", "Quantitative analysis with predictions and backtesting")
|
|
90
|
+
|
|
91
|
+
console.print(modes)
|
|
92
|
+
console.print()
|
|
93
|
+
|
|
94
|
+
# Examples
|
|
95
|
+
examples = Table(title="[bold bright_cyan]Example Queries[/bold bright_cyan]", box=ROUNDED, show_header=False)
|
|
96
|
+
examples.add_column("Query", style="white")
|
|
97
|
+
|
|
98
|
+
examples.add_row("[dim]── Stock Analysis ──[/dim]")
|
|
99
|
+
examples.add_row(" Analyze NVDA stock")
|
|
100
|
+
examples.add_row(" Is AAPL a good investment?")
|
|
101
|
+
examples.add_row(" Give me a deep dive on Tesla")
|
|
102
|
+
examples.add_row("")
|
|
103
|
+
examples.add_row("[dim]── Charts & Visualization ──[/dim]")
|
|
104
|
+
examples.add_row(" Show me a chart of MSFT for the past 6 months")
|
|
105
|
+
examples.add_row(" Compare NVDA, AMD, and INTC")
|
|
106
|
+
examples.add_row(" Show RSI chart for SPY")
|
|
107
|
+
examples.add_row("")
|
|
108
|
+
examples.add_row("[dim]── Technical Analysis ──[/dim]")
|
|
109
|
+
examples.add_row(" Technical analysis on QQQ")
|
|
110
|
+
examples.add_row(" What are the support levels for META?")
|
|
111
|
+
examples.add_row(" Is GOOGL overbought?")
|
|
112
|
+
examples.add_row("")
|
|
113
|
+
examples.add_row("[dim]── Predictions & Sentiment ──[/dim]")
|
|
114
|
+
examples.add_row(" Predict AMZN price for next month")
|
|
115
|
+
examples.add_row(" What's the sentiment on TSLA?")
|
|
116
|
+
examples.add_row(" Price forecast for Bitcoin")
|
|
117
|
+
examples.add_row("")
|
|
118
|
+
examples.add_row("[dim]── Backtesting & Strategy ──[/dim]")
|
|
119
|
+
examples.add_row(" Create a MACD strategy backtest for AAPL")
|
|
120
|
+
examples.add_row(" Generate RSI mean reversion backtest for SPY")
|
|
121
|
+
examples.add_row(" List available backtest strategies")
|
|
122
|
+
examples.add_row("")
|
|
123
|
+
examples.add_row("[dim]── Market Overview ──[/dim]")
|
|
124
|
+
examples.add_row(" What are today's market movers?")
|
|
125
|
+
examples.add_row(" Show sector performance")
|
|
126
|
+
examples.add_row(" How are the major indices doing?")
|
|
127
|
+
|
|
128
|
+
console.print(examples)
|
|
129
|
+
console.print()
|
|
130
|
+
|
|
131
|
+
# Strategies
|
|
132
|
+
strategies = Table(title="[bold bright_cyan]Backtest Strategies[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
133
|
+
strategies.add_column("Strategy", style="bright_yellow")
|
|
134
|
+
strategies.add_column("Description", style="white")
|
|
135
|
+
|
|
136
|
+
strategies.add_row("sma_crossover", "Classic moving average crossover (fast/slow SMA)")
|
|
137
|
+
strategies.add_row("rsi_mean_reversion", "Buy oversold, sell overbought using RSI")
|
|
138
|
+
strategies.add_row("macd_momentum", "MACD histogram momentum strategy")
|
|
139
|
+
strategies.add_row("bollinger_bands", "Mean reversion at Bollinger Band extremes")
|
|
140
|
+
strategies.add_row("dual_momentum", "Absolute + relative momentum")
|
|
141
|
+
strategies.add_row("breakout", "Donchian channel breakout strategy")
|
|
142
|
+
|
|
143
|
+
console.print(strategies)
|
|
144
|
+
console.print()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Tool name mappings for cleaner display
|
|
148
|
+
TOOL_DISPLAY_NAMES = {
|
|
149
|
+
"get_stock_quote": "Stock Quote",
|
|
150
|
+
"get_stock_history": "Price History",
|
|
151
|
+
"get_company_info": "Company Info",
|
|
152
|
+
"get_financial_statements": "Financials",
|
|
153
|
+
"get_analyst_recommendations": "Analyst Ratings",
|
|
154
|
+
"get_insider_trades": "Insider Trades",
|
|
155
|
+
"get_institutional_holders": "Institutions",
|
|
156
|
+
"get_earnings_calendar": "Earnings",
|
|
157
|
+
"get_options_chain": "Options Chain",
|
|
158
|
+
"get_dividends": "Dividends",
|
|
159
|
+
"compare_stocks": "Compare Stocks",
|
|
160
|
+
"get_market_movers": "Market Movers",
|
|
161
|
+
"get_sector_performance": "Sectors",
|
|
162
|
+
"get_market_indices": "Market Indices",
|
|
163
|
+
"calculate_portfolio_metrics": "Portfolio",
|
|
164
|
+
"search_stocks": "Stock Search",
|
|
165
|
+
"get_stock_news": "News",
|
|
166
|
+
"technical_analysis": "Technicals",
|
|
167
|
+
"generate_price_chart": "Price Chart",
|
|
168
|
+
"generate_comparison_chart": "Comparison",
|
|
169
|
+
"generate_rsi_chart": "RSI Chart",
|
|
170
|
+
"generate_sector_chart": "Sectors",
|
|
171
|
+
"list_backtest_strategies": "Strategies",
|
|
172
|
+
"generate_backtest": "Backtest",
|
|
173
|
+
"generate_custom_backtest": "Custom Backtest",
|
|
174
|
+
"price_forecast": "Forecast",
|
|
175
|
+
"sentiment_analysis": "Sentiment",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
TOOL_ICONS = {
|
|
179
|
+
"get_stock_quote": ">",
|
|
180
|
+
"get_stock_history": ">",
|
|
181
|
+
"get_company_info": ">",
|
|
182
|
+
"get_financial_statements": ">",
|
|
183
|
+
"get_analyst_recommendations": ">",
|
|
184
|
+
"get_insider_trades": ">",
|
|
185
|
+
"get_institutional_holders": ">",
|
|
186
|
+
"get_earnings_calendar": ">",
|
|
187
|
+
"get_options_chain": ">",
|
|
188
|
+
"get_dividends": ">",
|
|
189
|
+
"compare_stocks": ">",
|
|
190
|
+
"get_market_movers": ">",
|
|
191
|
+
"get_sector_performance": ">",
|
|
192
|
+
"get_market_indices": ">",
|
|
193
|
+
"calculate_portfolio_metrics": ">",
|
|
194
|
+
"search_stocks": ">",
|
|
195
|
+
"get_stock_news": ">",
|
|
196
|
+
"technical_analysis": ">",
|
|
197
|
+
"generate_price_chart": ">",
|
|
198
|
+
"generate_comparison_chart": ">",
|
|
199
|
+
"generate_rsi_chart": ">",
|
|
200
|
+
"generate_sector_chart": ">",
|
|
201
|
+
"list_backtest_strategies": ">",
|
|
202
|
+
"generate_backtest": ">",
|
|
203
|
+
"generate_custom_backtest": ">",
|
|
204
|
+
"price_forecast": ">",
|
|
205
|
+
"sentiment_analysis": ">",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def format_tool_description(name: str, args: dict) -> tuple[str, Optional[str]]:
|
|
210
|
+
"""Format tool call with description. Returns (display_name, detail)."""
|
|
211
|
+
display_name = TOOL_DISPLAY_NAMES.get(name, name.replace('_', ' ').title())
|
|
212
|
+
icon = TOOL_ICONS.get(name, "●")
|
|
213
|
+
|
|
214
|
+
symbol = args.get('symbol', '')
|
|
215
|
+
symbols = args.get('symbols', [])
|
|
216
|
+
query = args.get('query', '')
|
|
217
|
+
strategy = args.get('strategy', '')
|
|
218
|
+
|
|
219
|
+
if symbol and strategy:
|
|
220
|
+
return f"{icon} {display_name}", f"{symbol.upper()} - {strategy}"
|
|
221
|
+
elif symbol:
|
|
222
|
+
return f"{icon} {display_name}", symbol.upper()
|
|
223
|
+
elif symbols:
|
|
224
|
+
return f"{icon} {display_name}", ', '.join(s.upper() for s in symbols[:3])
|
|
225
|
+
elif query:
|
|
226
|
+
return f"{icon} {display_name}", query[:30]
|
|
227
|
+
else:
|
|
228
|
+
return f"{icon} {display_name}", None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class SigmaUI:
|
|
232
|
+
"""Professional terminal UI for Sigma."""
|
|
233
|
+
|
|
234
|
+
def __init__(self):
|
|
235
|
+
self.settings = get_settings()
|
|
236
|
+
self.agent: Optional[SigmaAgent] = None
|
|
237
|
+
self.provider = self.settings.default_provider
|
|
238
|
+
self.tool_calls: list[tuple[str, float]] = []
|
|
239
|
+
self.start_time = 0.0
|
|
240
|
+
self.custom_model: Optional[str] = None
|
|
241
|
+
self.mode = "default" # default, technical, fundamental, quant
|
|
242
|
+
self.chart_output: Optional[str] = None # Store chart for display
|
|
243
|
+
|
|
244
|
+
def _init_agent(self):
|
|
245
|
+
"""Initialize agent."""
|
|
246
|
+
self.agent = SigmaAgent(provider=self.provider, model=self.custom_model)
|
|
247
|
+
|
|
248
|
+
def _get_model_display(self) -> str:
|
|
249
|
+
"""Get model name for display."""
|
|
250
|
+
if self.custom_model:
|
|
251
|
+
return self.custom_model
|
|
252
|
+
return self.settings.get_model(self.provider)
|
|
253
|
+
|
|
254
|
+
def on_tool_start(self, name: str, args: dict):
|
|
255
|
+
"""Called when tool starts."""
|
|
256
|
+
display_name, detail = format_tool_description(name, args)
|
|
257
|
+
if detail:
|
|
258
|
+
console.print(f" [bright_cyan]{display_name}[/bright_cyan] [dim]({detail})[/dim]")
|
|
259
|
+
else:
|
|
260
|
+
console.print(f" [bright_cyan]{display_name}[/bright_cyan]")
|
|
261
|
+
|
|
262
|
+
def on_tool_end(self, name: str, result: Any, duration_ms: float):
|
|
263
|
+
"""Called when tool ends."""
|
|
264
|
+
self.tool_calls.append((name, duration_ms))
|
|
265
|
+
|
|
266
|
+
# Check for chart output
|
|
267
|
+
if isinstance(result, dict) and result.get("display_as_chart"):
|
|
268
|
+
self.chart_output = result.get("chart", "")
|
|
269
|
+
|
|
270
|
+
def on_thinking(self, content: str):
|
|
271
|
+
"""Called when agent is thinking."""
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
def on_response(self, content: str):
|
|
275
|
+
"""Called with final response."""
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
async def process_query(self, query: str):
|
|
279
|
+
"""Process a query."""
|
|
280
|
+
if self.agent is None:
|
|
281
|
+
self._init_agent()
|
|
282
|
+
|
|
283
|
+
assert self.agent is not None
|
|
284
|
+
|
|
285
|
+
self.tool_calls = []
|
|
286
|
+
self.start_time = time.time()
|
|
287
|
+
self.chart_output = None
|
|
288
|
+
|
|
289
|
+
console.print()
|
|
290
|
+
console.print(" [dim]╭─ Researching...[/dim]")
|
|
291
|
+
|
|
292
|
+
# Run agent
|
|
293
|
+
response = await self.agent.run(
|
|
294
|
+
query,
|
|
295
|
+
on_tool_start=self.on_tool_start,
|
|
296
|
+
on_tool_end=self.on_tool_end,
|
|
297
|
+
on_thinking=self.on_thinking,
|
|
298
|
+
on_response=self.on_response,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
elapsed = time.time() - self.start_time
|
|
302
|
+
|
|
303
|
+
# Print data sources summary
|
|
304
|
+
if self.tool_calls:
|
|
305
|
+
console.print(f" [dim]╰─[/dim] [bright_green]Called {len(self.tool_calls)} data sources[/bright_green] [dim]in {elapsed:.1f}s[/dim]")
|
|
306
|
+
|
|
307
|
+
console.print()
|
|
308
|
+
|
|
309
|
+
# Print chart if generated
|
|
310
|
+
if self.chart_output:
|
|
311
|
+
console.print(self.chart_output)
|
|
312
|
+
console.print()
|
|
313
|
+
|
|
314
|
+
# Print response
|
|
315
|
+
self._display_response(response)
|
|
316
|
+
|
|
317
|
+
console.print()
|
|
318
|
+
|
|
319
|
+
def _display_response(self, response: str):
|
|
320
|
+
"""Display the response with professional formatting."""
|
|
321
|
+
lines = response.split('\n')
|
|
322
|
+
in_table = False
|
|
323
|
+
table_lines: list[str] = []
|
|
324
|
+
|
|
325
|
+
for line in lines:
|
|
326
|
+
# Check if this is a table line
|
|
327
|
+
if '|' in line and ('---' in line or line.strip().startswith('|')):
|
|
328
|
+
if not in_table:
|
|
329
|
+
in_table = True
|
|
330
|
+
table_lines = []
|
|
331
|
+
table_lines.append(line)
|
|
332
|
+
continue
|
|
333
|
+
elif in_table and line.strip():
|
|
334
|
+
if '|' in line:
|
|
335
|
+
table_lines.append(line)
|
|
336
|
+
continue
|
|
337
|
+
else:
|
|
338
|
+
self._print_table(table_lines)
|
|
339
|
+
in_table = False
|
|
340
|
+
table_lines = []
|
|
341
|
+
elif in_table and not line.strip():
|
|
342
|
+
self._print_table(table_lines)
|
|
343
|
+
in_table = False
|
|
344
|
+
table_lines = []
|
|
345
|
+
|
|
346
|
+
# Format and print the line
|
|
347
|
+
formatted = self._format_line(line)
|
|
348
|
+
if formatted is not None:
|
|
349
|
+
console.print(formatted)
|
|
350
|
+
|
|
351
|
+
# Print any remaining table
|
|
352
|
+
if table_lines:
|
|
353
|
+
self._print_table(table_lines)
|
|
354
|
+
|
|
355
|
+
def _format_line(self, line: str) -> Optional[str]:
|
|
356
|
+
"""Format a single line with proper markdown rendering."""
|
|
357
|
+
stripped = line.strip()
|
|
358
|
+
|
|
359
|
+
# Empty line
|
|
360
|
+
if not stripped:
|
|
361
|
+
return ""
|
|
362
|
+
|
|
363
|
+
# Headers
|
|
364
|
+
if stripped.startswith('#'):
|
|
365
|
+
header_match = re.match(r'^(#+)\s*(.+)$', stripped)
|
|
366
|
+
if header_match:
|
|
367
|
+
level = len(header_match.group(1))
|
|
368
|
+
text = header_match.group(2)
|
|
369
|
+
if level == 1:
|
|
370
|
+
return f"\n[bold bright_white]{text}[/bold bright_white]"
|
|
371
|
+
elif level == 2:
|
|
372
|
+
return f"\n[bold bright_cyan]▸ {text}[/bold bright_cyan]"
|
|
373
|
+
else:
|
|
374
|
+
return f"\n[bold]{text}[/bold]"
|
|
375
|
+
|
|
376
|
+
# Bold-only lines
|
|
377
|
+
if stripped.startswith('**') and stripped.endswith('**') and stripped.count('**') == 2:
|
|
378
|
+
text = stripped[2:-2]
|
|
379
|
+
return f"\n[bold bright_yellow]▸ {text}[/bold bright_yellow]"
|
|
380
|
+
|
|
381
|
+
# Lines starting with bold
|
|
382
|
+
bold_start = re.match(r'^\*\*(.+?)\*\*(.*)$', stripped)
|
|
383
|
+
if bold_start:
|
|
384
|
+
bold_part = bold_start.group(1)
|
|
385
|
+
rest = bold_start.group(2)
|
|
386
|
+
rest = re.sub(r'\*\*(.+?)\*\*', r'[bold]\1[/bold]', rest)
|
|
387
|
+
rest = re.sub(r'`([^`]+)`', r'[bright_cyan]\1[/bright_cyan]', rest)
|
|
388
|
+
return f"[bold bright_green]{bold_part}[/bold bright_green]{rest}"
|
|
389
|
+
|
|
390
|
+
# Bullet points
|
|
391
|
+
if stripped.startswith('- ') or stripped.startswith('* '):
|
|
392
|
+
bullet_text = stripped[2:]
|
|
393
|
+
bullet_text = re.sub(r'\*\*(.+?)\*\*', r'[bold]\1[/bold]', bullet_text)
|
|
394
|
+
bullet_text = re.sub(r'`([^`]+)`', r'[bright_cyan]\1[/bright_cyan]', bullet_text)
|
|
395
|
+
return f" [dim]•[/dim] {bullet_text}"
|
|
396
|
+
|
|
397
|
+
# Numbered lists
|
|
398
|
+
numbered = re.match(r'^(\d+)\.\s+(.+)$', stripped)
|
|
399
|
+
if numbered:
|
|
400
|
+
num = numbered.group(1)
|
|
401
|
+
text = numbered.group(2)
|
|
402
|
+
text = re.sub(r'\*\*(.+?)\*\*', r'[bold]\1[/bold]', text)
|
|
403
|
+
text = re.sub(r'`([^`]+)`', r'[bright_cyan]\1[/bright_cyan]', text)
|
|
404
|
+
return f" [bright_cyan]{num}.[/bright_cyan] {text}"
|
|
405
|
+
|
|
406
|
+
# Regular line - handle inline formatting
|
|
407
|
+
formatted = stripped
|
|
408
|
+
formatted = re.sub(r'\*\*(.+?)\*\*', r'[bold]\1[/bold]', formatted)
|
|
409
|
+
formatted = re.sub(r'`([^`]+)`', r'[bright_cyan]\1[/bright_cyan]', formatted)
|
|
410
|
+
|
|
411
|
+
return formatted
|
|
412
|
+
|
|
413
|
+
def _print_table(self, lines: list[str]):
|
|
414
|
+
"""Print a markdown table as a Rich table."""
|
|
415
|
+
if len(lines) < 2:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
header_line = lines[0]
|
|
419
|
+
headers = [h.strip() for h in header_line.split('|') if h.strip()]
|
|
420
|
+
|
|
421
|
+
table = Table(box=ROUNDED, show_header=True, header_style="bold bright_cyan")
|
|
422
|
+
for h in headers:
|
|
423
|
+
table.add_column(h)
|
|
424
|
+
|
|
425
|
+
for line in lines[2:]:
|
|
426
|
+
if '---' in line:
|
|
427
|
+
continue
|
|
428
|
+
cells = [c.strip() for c in line.split('|') if c.strip()]
|
|
429
|
+
if cells:
|
|
430
|
+
# Color code values
|
|
431
|
+
colored_cells = []
|
|
432
|
+
for cell in cells:
|
|
433
|
+
if cell.startswith('+') or cell.lower() in ['buy', 'bullish', 'strong buy']:
|
|
434
|
+
colored_cells.append(f"[green]{cell}[/green]")
|
|
435
|
+
elif cell.startswith('-') or cell.lower() in ['sell', 'bearish']:
|
|
436
|
+
colored_cells.append(f"[red]{cell}[/red]")
|
|
437
|
+
else:
|
|
438
|
+
colored_cells.append(cell)
|
|
439
|
+
table.add_row(*colored_cells)
|
|
440
|
+
|
|
441
|
+
console.print()
|
|
442
|
+
console.print(Padding(table, (0, 2)))
|
|
443
|
+
|
|
444
|
+
async def quick_chart(self, symbol: str, period: str = "3mo"):
|
|
445
|
+
"""Generate a quick chart."""
|
|
446
|
+
from sigma.tools.charts import create_price_chart
|
|
447
|
+
chart = create_price_chart(symbol, period)
|
|
448
|
+
console.print()
|
|
449
|
+
console.print(chart)
|
|
450
|
+
console.print()
|
|
451
|
+
|
|
452
|
+
async def quick_compare(self, symbols: list[str], period: str = "3mo"):
|
|
453
|
+
"""Generate a quick comparison chart."""
|
|
454
|
+
from sigma.tools.charts import create_comparison_chart
|
|
455
|
+
chart = create_comparison_chart(symbols, period)
|
|
456
|
+
console.print()
|
|
457
|
+
console.print(chart)
|
|
458
|
+
console.print()
|
|
459
|
+
|
|
460
|
+
def _handle_lean_command(self, args: list[str]):
|
|
461
|
+
"""Handle /lean commands for LEAN Engine backtesting."""
|
|
462
|
+
from sigma.tools.backtest import setup_lean_engine, run_lean_backtest, check_lean_status, get_available_strategies
|
|
463
|
+
|
|
464
|
+
if not args:
|
|
465
|
+
# Show help menu
|
|
466
|
+
strategies = get_available_strategies()
|
|
467
|
+
console.print()
|
|
468
|
+
console.print(Panel(
|
|
469
|
+
"\n".join([
|
|
470
|
+
"[bold cyan]LEAN Engine Backtesting[/bold cyan]",
|
|
471
|
+
"",
|
|
472
|
+
"[bold]Commands:[/bold]",
|
|
473
|
+
" /lean setup - Setup LEAN Engine (one-time)",
|
|
474
|
+
" /lean run <SYM> <STRAT> - Run comprehensive backtest",
|
|
475
|
+
" /lean status - Check LEAN setup status",
|
|
476
|
+
" /lean strategies - List available strategies",
|
|
477
|
+
"",
|
|
478
|
+
"[bold]Quick Start:[/bold]",
|
|
479
|
+
" /lean run AAPL sma_crossover",
|
|
480
|
+
" /lean run TSLA macd_momentum",
|
|
481
|
+
" /lean run NVDA rsi_mean_reversion",
|
|
482
|
+
"",
|
|
483
|
+
"[bold]Available Strategies:[/bold]",
|
|
484
|
+
*[f" [cyan]{name}[/cyan] - {s['description'][:50]}..." for name, s in list(strategies.items())[:3]],
|
|
485
|
+
" [dim]...use /lean strategies for full list[/dim]"
|
|
486
|
+
]),
|
|
487
|
+
title="[bold bright_cyan]LEAN Backtest Engine[/bold bright_cyan]",
|
|
488
|
+
border_style="bright_blue"
|
|
489
|
+
))
|
|
490
|
+
console.print()
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
subcmd = args[0].lower()
|
|
494
|
+
|
|
495
|
+
if subcmd == "help":
|
|
496
|
+
self._handle_lean_command([])
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
if subcmd == "strategies":
|
|
500
|
+
strategies = get_available_strategies()
|
|
501
|
+
console.print()
|
|
502
|
+
from rich.table import Table
|
|
503
|
+
table = Table(title="Available Backtest Strategies", border_style="bright_blue")
|
|
504
|
+
table.add_column("Strategy", style="cyan")
|
|
505
|
+
table.add_column("Description", style="dim")
|
|
506
|
+
table.add_column("Parameters", style="yellow")
|
|
507
|
+
|
|
508
|
+
for name, s in strategies.items():
|
|
509
|
+
params = ", ".join([f"{k}={v}" for k, v in s.get("default_params", {}).items()])
|
|
510
|
+
table.add_row(name, s["description"][:60], params[:40])
|
|
511
|
+
|
|
512
|
+
console.print(table)
|
|
513
|
+
console.print()
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
if subcmd == "status":
|
|
517
|
+
status = check_lean_status()
|
|
518
|
+
console.print()
|
|
519
|
+
console.print(Panel(
|
|
520
|
+
"\n".join([
|
|
521
|
+
f"[dim]Docker Installed:[/dim] {'[green]Yes[/green]' if status['docker_installed'] else '[red]No[/red]'}",
|
|
522
|
+
f"[dim]Docker Running:[/dim] {'[green]Yes[/green]' if status['docker_running'] else '[red]No[/red]'}",
|
|
523
|
+
f"[dim]LEAN Image:[/dim] {'[green]Pulled[/green]' if status['lean_image_pulled'] else '[yellow]Not pulled[/yellow]'}",
|
|
524
|
+
f"[dim]Workspace:[/dim] {'[green]Ready[/green]' if status['workspace_initialized'] else '[yellow]Not initialized[/yellow]'}",
|
|
525
|
+
"",
|
|
526
|
+
*status['instructions']
|
|
527
|
+
]),
|
|
528
|
+
title="[bold bright_cyan]LEAN Engine Status[/bold bright_cyan]",
|
|
529
|
+
border_style="bright_blue"
|
|
530
|
+
))
|
|
531
|
+
console.print()
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if subcmd == "setup":
|
|
535
|
+
console.print()
|
|
536
|
+
console.print("[bright_cyan]Setting up LEAN Engine...[/bright_cyan]")
|
|
537
|
+
console.print()
|
|
538
|
+
|
|
539
|
+
result = setup_lean_engine()
|
|
540
|
+
|
|
541
|
+
for step in result["steps_completed"]:
|
|
542
|
+
console.print(f" [green]✓[/green] {step}")
|
|
543
|
+
|
|
544
|
+
for error in result.get("errors", []):
|
|
545
|
+
console.print(f" [red]✗[/red] {error}")
|
|
546
|
+
|
|
547
|
+
console.print()
|
|
548
|
+
if result["success"]:
|
|
549
|
+
console.print(Panel(
|
|
550
|
+
"\n".join(result["next_steps"]),
|
|
551
|
+
title="[bold bright_green]Setup Complete[/bold bright_green]",
|
|
552
|
+
border_style="green"
|
|
553
|
+
))
|
|
554
|
+
else:
|
|
555
|
+
console.print(Panel(
|
|
556
|
+
"\n".join(result.get("next_steps", ["Setup failed. Check errors above."])),
|
|
557
|
+
title="[bold red]Setup Incomplete[/bold red]",
|
|
558
|
+
border_style="red"
|
|
559
|
+
))
|
|
560
|
+
console.print()
|
|
561
|
+
|
|
562
|
+
elif subcmd == "run":
|
|
563
|
+
if len(args) < 3:
|
|
564
|
+
console.print("\n [red]Usage:[/red] /lean run <symbol> <strategy>\n")
|
|
565
|
+
console.print(" [dim]Strategies:[/dim] sma_crossover, rsi_mean_reversion, macd_momentum, bollinger_bands, dual_momentum, breakout\n")
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
symbol = args[1].upper()
|
|
569
|
+
strategy = args[2].lower()
|
|
570
|
+
|
|
571
|
+
console.print()
|
|
572
|
+
console.print(f"[bright_cyan]Running Comprehensive Backtest: {symbol} - {strategy}[/bright_cyan]")
|
|
573
|
+
console.print()
|
|
574
|
+
|
|
575
|
+
result = run_lean_backtest(symbol, strategy)
|
|
576
|
+
|
|
577
|
+
for step in result["steps"]:
|
|
578
|
+
console.print(f" [dim]>[/dim] {step}")
|
|
579
|
+
|
|
580
|
+
console.print()
|
|
581
|
+
|
|
582
|
+
if result["status"] == "success":
|
|
583
|
+
metrics = result.get("metrics", {})
|
|
584
|
+
|
|
585
|
+
# Performance metrics panel
|
|
586
|
+
perf = metrics.get("performance", {})
|
|
587
|
+
console.print(Panel(
|
|
588
|
+
"\n".join([
|
|
589
|
+
f" [bold cyan]Initial Capital:[/bold cyan] {perf.get('initial_capital', 'N/A')}",
|
|
590
|
+
f" [bold cyan]Final Equity:[/bold cyan] {perf.get('final_equity', 'N/A')}",
|
|
591
|
+
f" [bold cyan]Total Return:[/bold cyan] {perf.get('total_return', 'N/A')}",
|
|
592
|
+
f" [bold cyan]Annual Return:[/bold cyan] {perf.get('annual_return', 'N/A')}",
|
|
593
|
+
f" [bold cyan]Buy & Hold:[/bold cyan] {perf.get('buy_hold_return', 'N/A')}",
|
|
594
|
+
f" [bold cyan]Alpha:[/bold cyan] {perf.get('alpha', 'N/A')}",
|
|
595
|
+
]),
|
|
596
|
+
title=f"[bold bright_green]Performance - {symbol} {strategy}[/bold bright_green]",
|
|
597
|
+
border_style="green"
|
|
598
|
+
))
|
|
599
|
+
|
|
600
|
+
# Risk metrics panel
|
|
601
|
+
risk = metrics.get("risk", {})
|
|
602
|
+
console.print(Panel(
|
|
603
|
+
"\n".join([
|
|
604
|
+
f" [bold yellow]Max Drawdown:[/bold yellow] {risk.get('max_drawdown', 'N/A')}",
|
|
605
|
+
f" [bold yellow]Volatility:[/bold yellow] {risk.get('volatility', 'N/A')}",
|
|
606
|
+
f" [bold yellow]Sharpe Ratio:[/bold yellow] {risk.get('sharpe_ratio', 'N/A')}",
|
|
607
|
+
f" [bold yellow]Sortino Ratio:[/bold yellow] {risk.get('sortino_ratio', 'N/A')}",
|
|
608
|
+
f" [bold yellow]Calmar Ratio:[/bold yellow] {risk.get('calmar_ratio', 'N/A')}",
|
|
609
|
+
]),
|
|
610
|
+
title="[bold bright_yellow]Risk Metrics[/bold bright_yellow]",
|
|
611
|
+
border_style="yellow"
|
|
612
|
+
))
|
|
613
|
+
|
|
614
|
+
# Trade statistics panel
|
|
615
|
+
trades = metrics.get("trades", {})
|
|
616
|
+
console.print(Panel(
|
|
617
|
+
"\n".join([
|
|
618
|
+
f" [bold magenta]Total Trades:[/bold magenta] {trades.get('total_trades', 'N/A')}",
|
|
619
|
+
f" [bold magenta]Win Rate:[/bold magenta] {trades.get('win_rate', 'N/A')}",
|
|
620
|
+
f" [bold magenta]Profit Factor:[/bold magenta] {trades.get('profit_factor', 'N/A')}",
|
|
621
|
+
f" [bold magenta]Avg Win:[/bold magenta] {trades.get('avg_win', 'N/A')}",
|
|
622
|
+
f" [bold magenta]Avg Loss:[/bold magenta] {trades.get('avg_loss', 'N/A')}",
|
|
623
|
+
f" [bold magenta]Avg Holding:[/bold magenta] {trades.get('avg_holding_days', 'N/A')} days",
|
|
624
|
+
]),
|
|
625
|
+
title="[bold bright_magenta]Trade Statistics[/bold bright_magenta]",
|
|
626
|
+
border_style="magenta"
|
|
627
|
+
))
|
|
628
|
+
|
|
629
|
+
# Display charts
|
|
630
|
+
charts = result.get("charts", {})
|
|
631
|
+
|
|
632
|
+
if charts.get("equity_curve"):
|
|
633
|
+
console.print()
|
|
634
|
+
console.print(charts["equity_curve"])
|
|
635
|
+
|
|
636
|
+
if charts.get("drawdown"):
|
|
637
|
+
console.print()
|
|
638
|
+
console.print(charts["drawdown"])
|
|
639
|
+
|
|
640
|
+
if charts.get("trade_pnl"):
|
|
641
|
+
console.print()
|
|
642
|
+
console.print(charts["trade_pnl"])
|
|
643
|
+
|
|
644
|
+
if charts.get("monthly_returns"):
|
|
645
|
+
console.print()
|
|
646
|
+
console.print(charts["monthly_returns"])
|
|
647
|
+
|
|
648
|
+
# Recent trades table
|
|
649
|
+
trade_list = result.get("trades", [])
|
|
650
|
+
if trade_list:
|
|
651
|
+
console.print()
|
|
652
|
+
from rich.table import Table
|
|
653
|
+
table = Table(title="Recent Trades", border_style="dim")
|
|
654
|
+
table.add_column("Date", style="dim")
|
|
655
|
+
table.add_column("Action", style="bold")
|
|
656
|
+
table.add_column("Price", justify="right")
|
|
657
|
+
table.add_column("Shares", justify="right")
|
|
658
|
+
table.add_column("P&L", justify="right")
|
|
659
|
+
|
|
660
|
+
for trade in trade_list[-10:]:
|
|
661
|
+
action_style = "green" if trade.get("action") == "BUY" else "red"
|
|
662
|
+
pnl = trade.get("pnl")
|
|
663
|
+
pnl_str = f"${pnl:+,.2f}" if pnl else "-"
|
|
664
|
+
pnl_style = "green" if pnl and pnl > 0 else "red" if pnl else "dim"
|
|
665
|
+
table.add_row(
|
|
666
|
+
trade.get("date", ""),
|
|
667
|
+
f"[{action_style}]{trade.get('action', '')}[/{action_style}]",
|
|
668
|
+
f"${trade.get('price', 0):,.2f}",
|
|
669
|
+
str(trade.get("shares", "")),
|
|
670
|
+
f"[{pnl_style}]{pnl_str}[/{pnl_style}]"
|
|
671
|
+
)
|
|
672
|
+
console.print(table)
|
|
673
|
+
|
|
674
|
+
# Monthly returns
|
|
675
|
+
monthly = result.get("monthly_returns", [])
|
|
676
|
+
if monthly:
|
|
677
|
+
console.print()
|
|
678
|
+
from rich.table import Table
|
|
679
|
+
mtable = Table(title="Monthly Returns", border_style="dim")
|
|
680
|
+
mtable.add_column("Month", style="dim")
|
|
681
|
+
mtable.add_column("Return", justify="right")
|
|
682
|
+
|
|
683
|
+
for m in monthly[-12:]:
|
|
684
|
+
ret = m.get("return", 0)
|
|
685
|
+
ret_style = "green" if ret > 0 else "red"
|
|
686
|
+
mtable.add_row(m.get("month", ""), f"[{ret_style}]{ret:+.2f}%[/{ret_style}]")
|
|
687
|
+
console.print(mtable)
|
|
688
|
+
|
|
689
|
+
# QuantConnect instructions
|
|
690
|
+
console.print()
|
|
691
|
+
console.print(Panel(
|
|
692
|
+
"\n".join(result.get("quantconnect_instructions", [])),
|
|
693
|
+
title="[bold bright_blue]QuantConnect Cloud (Institutional Data)[/bold bright_blue]",
|
|
694
|
+
border_style="blue"
|
|
695
|
+
))
|
|
696
|
+
|
|
697
|
+
else:
|
|
698
|
+
console.print(Panel(
|
|
699
|
+
f"[red]Error:[/red] {result.get('error', 'Unknown error')[:200]}",
|
|
700
|
+
title="[bold red]Backtest Failed[/bold red]",
|
|
701
|
+
border_style="red"
|
|
702
|
+
))
|
|
703
|
+
|
|
704
|
+
# Always show the algorithm file location
|
|
705
|
+
if result.get("algorithm_file"):
|
|
706
|
+
console.print()
|
|
707
|
+
console.print(f" [dim]LEAN Algorithm saved:[/dim] {result['algorithm_file']}")
|
|
708
|
+
console.print()
|
|
709
|
+
|
|
710
|
+
elif subcmd == "status":
|
|
711
|
+
self._handle_lean_command([]) # Same as no args
|
|
712
|
+
|
|
713
|
+
else:
|
|
714
|
+
console.print(f"\n [red]Unknown lean command:[/red] {subcmd}")
|
|
715
|
+
console.print(" [dim]Available:[/dim] /lean setup, /lean run <symbol> <strategy>, /lean status\n")
|
|
716
|
+
|
|
717
|
+
def handle_command(self, cmd: str) -> bool:
|
|
718
|
+
"""Handle slash command. Returns True to continue, False to quit."""
|
|
719
|
+
parts = cmd.strip().split()
|
|
720
|
+
command = parts[0].lower()
|
|
721
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
722
|
+
|
|
723
|
+
if command in ["/quit", "/exit", "/q"]:
|
|
724
|
+
return False
|
|
725
|
+
|
|
726
|
+
elif command in ["/help", "/h", "/?"]:
|
|
727
|
+
print_help()
|
|
728
|
+
|
|
729
|
+
elif command == "/model":
|
|
730
|
+
if not args:
|
|
731
|
+
available = self.settings.get_available_providers()
|
|
732
|
+
console.print()
|
|
733
|
+
console.print(f" [dim]Current model:[/dim] [bright_cyan]{self._get_model_display()}[/bright_cyan]")
|
|
734
|
+
console.print(f" [dim]Available:[/dim] {', '.join(p.value for p in available)}")
|
|
735
|
+
console.print()
|
|
736
|
+
else:
|
|
737
|
+
try:
|
|
738
|
+
self.provider = LLMProvider(args[0].lower())
|
|
739
|
+
self.agent = None
|
|
740
|
+
console.print(f"\n [bright_green]✓[/bright_green] Switched to [bright_cyan]{self._get_model_display()}[/bright_cyan]\n")
|
|
741
|
+
except ValueError:
|
|
742
|
+
console.print(f"\n [red]✗[/red] Unknown provider: {args[0]}\n")
|
|
743
|
+
|
|
744
|
+
elif command == "/mode":
|
|
745
|
+
if not args:
|
|
746
|
+
console.print()
|
|
747
|
+
console.print(f" [dim]Current mode:[/dim] [bright_cyan]{self.mode}[/bright_cyan]")
|
|
748
|
+
console.print(f" [dim]Available:[/dim] default, technical, fundamental, quant")
|
|
749
|
+
console.print()
|
|
750
|
+
else:
|
|
751
|
+
mode = args[0].lower()
|
|
752
|
+
if mode in ["default", "technical", "fundamental", "quant"]:
|
|
753
|
+
self.mode = mode
|
|
754
|
+
console.print(f"\n [bright_green]✓[/bright_green] Switched to [bright_cyan]{mode}[/bright_cyan] mode\n")
|
|
755
|
+
else:
|
|
756
|
+
console.print(f"\n [red]✗[/red] Unknown mode: {mode}\n")
|
|
757
|
+
|
|
758
|
+
elif command == "/clear":
|
|
759
|
+
if self.agent:
|
|
760
|
+
self.agent.clear()
|
|
761
|
+
console.print("\n [bright_green]✓[/bright_green] Conversation cleared\n")
|
|
762
|
+
|
|
763
|
+
elif command == "/status":
|
|
764
|
+
console.print()
|
|
765
|
+
console.print(f" [dim]Provider:[/dim] [bright_cyan]{self.provider.value}[/bright_cyan]")
|
|
766
|
+
console.print(f" [dim]Model:[/dim] [bright_cyan]{self._get_model_display()}[/bright_cyan]")
|
|
767
|
+
console.print(f" [dim]Mode:[/dim] [bright_cyan]{self.mode}[/bright_cyan]")
|
|
768
|
+
available = self.settings.get_available_providers()
|
|
769
|
+
console.print(f" [dim]Available providers:[/dim] {', '.join(p.value for p in available)}")
|
|
770
|
+
if self.agent:
|
|
771
|
+
stats = self.agent.get_stats()
|
|
772
|
+
console.print(f" [dim]Tools called this session:[/dim] {stats['tools_called']}")
|
|
773
|
+
console.print()
|
|
774
|
+
|
|
775
|
+
elif command == "/chart":
|
|
776
|
+
if not args:
|
|
777
|
+
console.print("\n [red]Usage:[/red] /chart <symbol> [period]\n")
|
|
778
|
+
else:
|
|
779
|
+
symbol = args[0].upper()
|
|
780
|
+
period = args[1] if len(args) > 1 else "3mo"
|
|
781
|
+
try:
|
|
782
|
+
asyncio.get_event_loop().run_until_complete(self.quick_chart(symbol, period))
|
|
783
|
+
except RuntimeError:
|
|
784
|
+
asyncio.run(self.quick_chart(symbol, period))
|
|
785
|
+
|
|
786
|
+
elif command == "/compare":
|
|
787
|
+
if len(args) < 2:
|
|
788
|
+
console.print("\n [red]Usage:[/red] /compare <symbol1> <symbol2> ...\n")
|
|
789
|
+
else:
|
|
790
|
+
symbols = [s.upper() for s in args[:5]]
|
|
791
|
+
try:
|
|
792
|
+
asyncio.get_event_loop().run_until_complete(self.quick_compare(symbols))
|
|
793
|
+
except RuntimeError:
|
|
794
|
+
asyncio.run(self.quick_compare(symbols))
|
|
795
|
+
|
|
796
|
+
elif command == "/backtest":
|
|
797
|
+
if len(args) < 2:
|
|
798
|
+
console.print("\n [red]Usage:[/red] /backtest <symbol> <strategy>\n")
|
|
799
|
+
console.print(" [dim]Strategies:[/dim] sma_crossover, rsi_mean_reversion, macd_momentum, bollinger_bands, dual_momentum, breakout\n")
|
|
800
|
+
else:
|
|
801
|
+
symbol = args[0]
|
|
802
|
+
strategy = args[1]
|
|
803
|
+
try:
|
|
804
|
+
loop = asyncio.get_event_loop()
|
|
805
|
+
loop.run_until_complete(
|
|
806
|
+
self.process_query(f"Generate a {strategy} backtest for {symbol}")
|
|
807
|
+
)
|
|
808
|
+
except RuntimeError:
|
|
809
|
+
asyncio.run(
|
|
810
|
+
self.process_query(f"Generate a {strategy} backtest for {symbol}")
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
elif command == "/lean":
|
|
814
|
+
self._handle_lean_command(args)
|
|
815
|
+
|
|
816
|
+
else:
|
|
817
|
+
console.print(f"\n [red]Unknown command:[/red] {command}. Type /help for commands.\n")
|
|
818
|
+
|
|
819
|
+
return True
|
|
820
|
+
|
|
821
|
+
async def run(self):
|
|
822
|
+
"""Run the interactive loop."""
|
|
823
|
+
print_banner(self._get_model_display())
|
|
824
|
+
|
|
825
|
+
while True:
|
|
826
|
+
try:
|
|
827
|
+
# Professional prompt
|
|
828
|
+
prompt_line = f"[bold bright_yellow]σ[/bold bright_yellow] [dim]›[/dim] "
|
|
829
|
+
query = console.input(prompt_line).strip()
|
|
830
|
+
|
|
831
|
+
if not query:
|
|
832
|
+
continue
|
|
833
|
+
|
|
834
|
+
# Handle commands
|
|
835
|
+
if query.startswith("/"):
|
|
836
|
+
if not self.handle_command(query):
|
|
837
|
+
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim] 📈\n")
|
|
838
|
+
break
|
|
839
|
+
continue
|
|
840
|
+
|
|
841
|
+
# Process query
|
|
842
|
+
await self.process_query(query)
|
|
843
|
+
|
|
844
|
+
except KeyboardInterrupt:
|
|
845
|
+
console.print("\n")
|
|
846
|
+
continue
|
|
847
|
+
except EOFError:
|
|
848
|
+
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim] 📈\n")
|
|
849
|
+
break
|
|
850
|
+
except Exception as e:
|
|
851
|
+
console.print(f"\n [red]Error:[/red] {str(e)}\n")
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def main():
|
|
855
|
+
"""Main entry point."""
|
|
856
|
+
import argparse
|
|
857
|
+
|
|
858
|
+
parser = argparse.ArgumentParser(
|
|
859
|
+
prog="sigma",
|
|
860
|
+
description="Sigma - Institutional-Grade Financial Research Agent",
|
|
861
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
862
|
+
epilog="""
|
|
863
|
+
Examples:
|
|
864
|
+
sigma Start interactive mode
|
|
865
|
+
sigma --setup Run setup wizard
|
|
866
|
+
sigma "Analyze AAPL" Direct query mode
|
|
867
|
+
sigma --version Show version
|
|
868
|
+
|
|
869
|
+
Inside Sigma:
|
|
870
|
+
/help Show all commands
|
|
871
|
+
/model openai Switch to OpenAI
|
|
872
|
+
/lean run TSLA macd Run backtest
|
|
873
|
+
"""
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
parser.add_argument(
|
|
877
|
+
"query",
|
|
878
|
+
nargs="?",
|
|
879
|
+
help="Direct query to analyze (optional)"
|
|
880
|
+
)
|
|
881
|
+
parser.add_argument(
|
|
882
|
+
"--setup",
|
|
883
|
+
action="store_true",
|
|
884
|
+
help="Run the setup wizard"
|
|
885
|
+
)
|
|
886
|
+
parser.add_argument(
|
|
887
|
+
"--reset",
|
|
888
|
+
action="store_true",
|
|
889
|
+
help="Reset configuration and run setup"
|
|
890
|
+
)
|
|
891
|
+
parser.add_argument(
|
|
892
|
+
"--version", "-v",
|
|
893
|
+
action="version",
|
|
894
|
+
version=f"Sigma {VERSION}"
|
|
895
|
+
)
|
|
896
|
+
parser.add_argument(
|
|
897
|
+
"--model", "-m",
|
|
898
|
+
choices=["openai", "anthropic", "google", "groq", "xai", "ollama"],
|
|
899
|
+
help="Override default AI model"
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
args = parser.parse_args()
|
|
903
|
+
|
|
904
|
+
# Handle setup
|
|
905
|
+
from sigma.setup import ensure_setup, run_setup, is_setup_complete, CONFIG_DIR
|
|
906
|
+
import shutil
|
|
907
|
+
|
|
908
|
+
if args.reset:
|
|
909
|
+
if CONFIG_DIR.exists():
|
|
910
|
+
shutil.rmtree(CONFIG_DIR)
|
|
911
|
+
run_setup(force=True)
|
|
912
|
+
elif args.setup:
|
|
913
|
+
run_setup(force=True)
|
|
914
|
+
else:
|
|
915
|
+
# Ensure setup is done
|
|
916
|
+
ensure_setup()
|
|
917
|
+
|
|
918
|
+
# Create UI with optional model override
|
|
919
|
+
ui = SigmaUI()
|
|
920
|
+
if args.model:
|
|
921
|
+
try:
|
|
922
|
+
ui.provider = LLMProvider(args.model)
|
|
923
|
+
ui.agent = None # Force agent reload
|
|
924
|
+
except ValueError:
|
|
925
|
+
pass
|
|
926
|
+
|
|
927
|
+
# Handle direct query or interactive mode
|
|
928
|
+
if args.query:
|
|
929
|
+
# Direct query mode
|
|
930
|
+
async def run_query():
|
|
931
|
+
print_banner(ui._get_model_display())
|
|
932
|
+
await ui.process_query(args.query)
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
asyncio.run(run_query())
|
|
936
|
+
except KeyboardInterrupt:
|
|
937
|
+
console.print("\n")
|
|
938
|
+
else:
|
|
939
|
+
# Interactive mode
|
|
940
|
+
try:
|
|
941
|
+
asyncio.run(ui.run())
|
|
942
|
+
except KeyboardInterrupt:
|
|
943
|
+
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim]\n")
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
if __name__ == "__main__":
|
|
947
|
+
main()
|