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/app.py CHANGED
@@ -1,947 +1,856 @@
1
- """Sigma CLI - Professional financial research interface."""
1
+ """Sigma v3.3.0 - Finance Research Agent."""
2
2
 
3
3
  import asyncio
4
- import sys
4
+ import os
5
5
  import re
6
- from typing import Any, Optional
7
- import time
6
+ from datetime import datetime
7
+ from typing import Optional, List
8
8
 
9
- from rich.console import Console
9
+ from rich.markdown import Markdown
10
10
  from rich.panel import Panel
11
- from rich.text import Text
12
11
  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
12
+ from rich.text import Text
13
+ from textual import on, work
14
+ from textual.app import App, ComposeResult
15
+ from textual.binding import Binding
16
+ from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
17
+ from textual.widgets import Footer, Input, RichLog, Static
18
+ from textual.suggester import Suggester
19
+
20
+ from .config import LLMProvider, get_settings, save_api_key, AVAILABLE_MODELS
21
+ from .llm import get_llm
22
+ from .tools import TOOLS, execute_tool
23
+ from .backtest import run_backtest, get_available_strategies, BACKTEST_TOOL
24
+
18
25
 
19
- from sigma.core.agent import SigmaAgent
20
- from sigma.core.config import LLMProvider, get_settings
26
+ __version__ = "3.3.0"
27
+ SIGMA = "σ"
21
28
 
29
+ # Common stock tickers for recognition
30
+ COMMON_TICKERS = {
31
+ "AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "META", "TSLA", "BRK.A", "BRK.B",
32
+ "JPM", "JNJ", "V", "PG", "UNH", "HD", "MA", "DIS", "PYPL", "BAC", "ADBE", "NFLX",
33
+ "CRM", "INTC", "AMD", "CSCO", "PEP", "KO", "ABT", "NKE", "MRK", "PFE", "TMO",
34
+ "COST", "AVGO", "WMT", "ACN", "LLY", "MCD", "DHR", "TXN", "NEE", "PM", "HON",
35
+ "UPS", "BMY", "QCOM", "LOW", "MS", "RTX", "UNP", "ORCL", "IBM", "GE", "CAT",
36
+ "SBUX", "AMAT", "GS", "BLK", "DE", "AMT", "NOW", "ISRG", "LMT", "MDLZ", "AXP",
37
+ "SYK", "BKNG", "PLD", "GILD", "ADI", "TMUS", "CVS", "MMC", "ZTS", "CB", "C",
38
+ "SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK", "XLE",
39
+ }
40
+
41
+ # Small sigma animation frames (minimal footprint)
42
+ SMALL_SIGMA_FRAMES = [
43
+ "[bold blue]σ[/bold blue]",
44
+ "[bold cyan]σ[/bold cyan]",
45
+ "[bold white]σ[/bold white]",
46
+ "[bold #60a5fa]σ[/bold #60a5fa]",
47
+ ]
22
48
 
23
- console = Console()
49
+ # Tool call animation frames
50
+ TOOL_CALL_FRAMES = [
51
+ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
52
+ ]
24
53
 
25
- # Version
26
- VERSION = "2.0.2"
54
+ # Welcome banner - clean design
55
+ WELCOME_BANNER = """
56
+ [bold blue]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold blue]
57
+ [bold blue]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold blue]
58
+ [bold blue]███████╗██║██║ ███╗██╔████╔██║███████║[/bold blue]
59
+ [bold blue]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold blue]
60
+ [bold blue]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold blue]
61
+ [bold blue]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold blue]
27
62
 
28
- # ASCII Art Banner
29
- BANNER = """[bold bright_blue]
30
- ███████╗██╗ ██████╗ ███╗ ███╗ █████╗
31
- ██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
32
- ███████╗██║██║ ███╗██╔████╔██║███████║
33
- ╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
34
- ███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
35
- ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold bright_blue]
63
+ [bold cyan]Finance Research Agent[/bold cyan] [dim]v3.3.0 | Native macOS[/dim]
36
64
  """
37
65
 
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
- }
66
+ SYSTEM_PROMPT = """You are Sigma, a Finance Research Agent. You provide comprehensive market analysis, trading strategies, and investment insights.
177
67
 
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
- }
68
+ CORE CAPABILITIES:
69
+ - Real-time market data analysis (quotes, charts, technicals)
70
+ - Fundamental analysis (financials, ratios, earnings)
71
+ - Technical analysis (RSI, MACD, Bollinger Bands, moving averages)
72
+ - Backtesting strategies (SMA crossover, RSI, MACD, Bollinger, momentum, breakout)
73
+ - Portfolio analysis and optimization
74
+ - Sector and market overview
75
+ - Insider and fund activity tracking
207
76
 
77
+ RESPONSE STYLE:
78
+ - Be concise and data-driven
79
+ - Lead with key insights, then supporting data
80
+ - Use tables for comparative data when appropriate
81
+ - Always cite specific numbers and metrics
82
+ - Provide actionable recommendations when asked
83
+ - Format currency and percentages properly
84
+ - Use STRONG BUY, BUY, HOLD, SELL, STRONG SELL ratings when appropriate
208
85
 
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
86
+ When users ask about stocks, always gather current data using your tools before responding."""
229
87
 
88
+ # Enhanced autocomplete suggestions with more variety
89
+ SUGGESTIONS = [
90
+ # Analysis commands
91
+ "analyze AAPL",
92
+ "analyze MSFT",
93
+ "analyze GOOGL",
94
+ "analyze NVDA",
95
+ "analyze TSLA",
96
+ "analyze META",
97
+ "analyze AMZN",
98
+ "analyze AMD",
99
+ "analyze SPY",
100
+ # Comparisons
101
+ "compare AAPL MSFT GOOGL",
102
+ "compare NVDA AMD INTC",
103
+ "compare META GOOGL AMZN",
104
+ "compare TSLA RIVN LCID",
105
+ # Technical
106
+ "technical analysis of AAPL",
107
+ "technical analysis of SPY",
108
+ "technical analysis of NVDA",
109
+ "technical analysis of QQQ",
110
+ # Backtesting
111
+ "backtest SMA crossover on AAPL",
112
+ "backtest RSI strategy on SPY",
113
+ "backtest MACD on NVDA",
114
+ "backtest momentum on QQQ",
115
+ # Market
116
+ "market overview",
117
+ "sector performance",
118
+ "what sectors are hot today",
119
+ # Quotes
120
+ "get quote for AAPL",
121
+ "price of NVDA",
122
+ "how is TSLA doing",
123
+ # Fundamentals
124
+ "fundamentals of MSFT",
125
+ "financials for AAPL",
126
+ "earnings of NVDA",
127
+ # Activity
128
+ "insider trading for AAPL",
129
+ "institutional holders of NVDA",
130
+ "analyst recommendations for TSLA",
131
+ # Natural language queries
132
+ "what should I know about AAPL",
133
+ "is NVDA overvalued",
134
+ "best tech stocks right now",
135
+ "should I buy TSLA",
136
+ # Commands
137
+ "/help",
138
+ "/clear",
139
+ "/keys",
140
+ "/models",
141
+ "/status",
142
+ "/backtest",
143
+ ]
230
144
 
231
- class SigmaUI:
232
- """Professional terminal UI for Sigma."""
145
+
146
+ def extract_tickers(text: str) -> List[str]:
147
+ """Extract stock tickers from text."""
148
+ # Look for common patterns: $AAPL, or standalone uppercase words
149
+ # Only match if it's a known ticker or starts with $
150
+ words = text.upper().split()
151
+ tickers = []
152
+
153
+ for word in words:
154
+ # Clean the word
155
+ clean = word.strip('.,!?()[]{}":;')
156
+
157
+ # Check for $TICKER format
158
+ if clean.startswith('$'):
159
+ ticker = clean[1:]
160
+ if ticker and ticker.isalpha() and len(ticker) <= 5:
161
+ tickers.append(ticker)
162
+ # Check if it's a known ticker
163
+ elif clean in COMMON_TICKERS:
164
+ tickers.append(clean)
165
+
166
+ return list(dict.fromkeys(tickers)) # Dedupe while preserving order
167
+
168
+
169
+ class SigmaSuggester(Suggester):
170
+ """Enhanced autocomplete suggester with ticker recognition."""
233
171
 
234
172
  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]")
173
+ super().__init__(use_cache=True, case_sensitive=False)
261
174
 
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))
175
+ async def get_suggestion(self, value: str) -> Optional[str]:
176
+ """Get autocomplete suggestion."""
177
+ if not value or len(value) < 2:
178
+ return None
265
179
 
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()
180
+ value_lower = value.lower()
282
181
 
283
- assert self.agent is not None
182
+ # Check for ticker pattern (all caps or starts with $)
183
+ if value.startswith("$") or value.isupper():
184
+ ticker = value.lstrip("$").upper()
185
+ for common in COMMON_TICKERS:
186
+ if common.startswith(ticker) and common != ticker:
187
+ return f"analyze {common}"
284
188
 
285
- self.tool_calls = []
286
- self.start_time = time.time()
287
- self.chart_output = None
189
+ # Standard suggestions
190
+ for suggestion in SUGGESTIONS:
191
+ if suggestion.lower().startswith(value_lower):
192
+ return suggestion
288
193
 
289
- console.print()
290
- console.print(" [dim]╭─ Researching...[/dim]")
194
+ # Try partial match in middle of suggestion
195
+ for suggestion in SUGGESTIONS:
196
+ if value_lower in suggestion.lower():
197
+ return suggestion
291
198
 
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()
199
+ return None
200
+
201
+
202
+ CSS = """
203
+ Screen {
204
+ background: #0a0a0f;
205
+ }
206
+
207
+ * {
208
+ scrollbar-size: 1 1;
209
+ scrollbar-color: #3b82f6 30%;
210
+ scrollbar-color-hover: #60a5fa 50%;
211
+ scrollbar-color-active: #93c5fd 70%;
212
+ }
213
+
214
+ #main-container {
215
+ width: 100%;
216
+ height: 100%;
217
+ background: #0a0a0f;
218
+ }
219
+
220
+ #chat-area {
221
+ height: 1fr;
222
+ margin: 1 2;
223
+ background: #0a0a0f;
224
+ }
225
+
226
+ #chat-log {
227
+ background: #0a0a0f;
228
+ padding: 1 0;
229
+ }
230
+
231
+ #status-bar {
232
+ height: 3;
233
+ background: #0d1117;
234
+ border-top: solid #1a1a2e;
235
+ padding: 0 2;
236
+ dock: bottom;
237
+ }
238
+
239
+ #status-content {
240
+ width: 100%;
241
+ height: 100%;
242
+ content-align: left middle;
243
+ }
244
+
245
+ #thinking-indicator {
246
+ width: auto;
247
+ height: 1;
248
+ content-align: center middle;
249
+ display: none;
250
+ }
251
+
252
+ #thinking-indicator.visible {
253
+ display: block;
254
+ }
255
+
256
+ #tool-calls-display {
257
+ width: 100%;
258
+ height: auto;
259
+ max-height: 6;
260
+ background: #0d1117;
261
+ border: solid #1a1a2e;
262
+ margin: 0 2;
263
+ padding: 0 1;
264
+ display: none;
265
+ }
266
+
267
+ #tool-calls-display.visible {
268
+ display: block;
269
+ }
270
+
271
+ #input-area {
272
+ height: 5;
273
+ padding: 1 2;
274
+ background: #0d1117;
275
+ }
276
+
277
+ #input-row {
278
+ height: 3;
279
+ width: 100%;
280
+ }
281
+
282
+ #sigma-indicator {
283
+ width: 4;
284
+ height: 3;
285
+ content-align: center middle;
286
+ }
287
+
288
+ #prompt-input {
289
+ width: 1fr;
290
+ background: #1a1a2e;
291
+ border: solid #3b82f6;
292
+ color: #ffffff;
293
+ padding: 0 1;
294
+ }
295
+
296
+ #prompt-input:focus {
297
+ border: solid #60a5fa;
298
+ }
299
+
300
+ #prompt-input.-autocomplete {
301
+ border: solid #22c55e;
302
+ }
303
+
304
+ #ticker-highlight {
305
+ width: auto;
306
+ height: 1;
307
+ padding: 0 1;
308
+ background: transparent;
309
+ }
310
+
311
+ Footer {
312
+ background: #0d1117;
313
+ height: 1;
314
+ dock: bottom;
315
+ }
316
+
317
+ Footer > .footer--highlight {
318
+ background: transparent;
319
+ }
320
+
321
+ Footer > .footer--key {
322
+ background: #1a1a2e;
323
+ color: #f59e0b;
324
+ text-style: bold;
325
+ }
326
+
327
+ Footer > .footer--description {
328
+ color: #6b7280;
329
+ }
330
+
331
+ #help-panel {
332
+ width: 100%;
333
+ height: auto;
334
+ padding: 1;
335
+ background: #0d1117;
336
+ border: solid #3b82f6;
337
+ margin: 1 2;
338
+ display: none;
339
+ }
340
+
341
+ #help-panel.visible {
342
+ display: block;
343
+ }
344
+ """
345
+
346
+
347
+ class ToolCallDisplay(Static):
348
+ """Animated display for tool calls."""
349
+
350
+ def __init__(self, *args, **kwargs):
351
+ super().__init__(*args, **kwargs)
352
+ self.tool_calls: List[dict] = []
353
+ self.frame = 0
354
+ self.timer = None
355
+
356
+ def add_tool_call(self, name: str, status: str = "running"):
357
+ """Add a tool call to the display."""
358
+ self.tool_calls.append({"name": name, "status": status, "frame": 0})
359
+ self.add_class("visible")
360
+ self._render()
361
+ if not self.timer:
362
+ self.timer = self.set_interval(0.1, self._animate)
363
+
364
+ def complete_tool_call(self, name: str):
365
+ """Mark a tool call as complete."""
366
+ for tc in self.tool_calls:
367
+ if tc["name"] == name and tc["status"] == "running":
368
+ tc["status"] = "complete"
369
+ break
370
+ self._render()
318
371
 
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] = []
372
+ def clear(self):
373
+ """Clear all tool calls."""
374
+ self.tool_calls = []
375
+ if self.timer:
376
+ self.timer.stop()
377
+ self.timer = None
378
+ self.remove_class("visible")
379
+ self.update("")
380
+
381
+ def _animate(self):
382
+ """Animate the spinner."""
383
+ self.frame = (self.frame + 1) % len(TOOL_CALL_FRAMES)
384
+ for tc in self.tool_calls:
385
+ if tc["status"] == "running":
386
+ tc["frame"] = self.frame
387
+ self._render()
388
+
389
+ def _render(self):
390
+ """Render the tool calls display."""
391
+ if not self.tool_calls:
392
+ return
324
393
 
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)
394
+ lines = []
395
+ for tc in self.tool_calls:
396
+ if tc["status"] == "running":
397
+ spinner = TOOL_CALL_FRAMES[tc["frame"]]
398
+ lines.append(f" [cyan]{spinner}[/cyan] [bold]{tc['name']}[/bold] [dim]executing...[/dim]")
399
+ else:
400
+ lines.append(f" [green]✓[/green] [bold]{tc['name']}[/bold] [green]complete[/green]")
350
401
 
351
- # Print any remaining table
352
- if table_lines:
353
- self._print_table(table_lines)
402
+ self.update(Text.from_markup("\n".join(lines)))
403
+
404
+
405
+ class SigmaIndicator(Static):
406
+ """Pulsing sigma indicator with minimal footprint."""
407
+
408
+ def __init__(self, *args, **kwargs):
409
+ super().__init__(*args, **kwargs)
410
+ self.active = False
411
+ self.frame = 0
412
+ self.timer = None
413
+
414
+ def on_mount(self):
415
+ self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
416
+
417
+ def set_active(self, active: bool):
418
+ self.active = active
419
+ if active and not self.timer:
420
+ self.timer = self.set_interval(0.15, self._pulse)
421
+ elif not active and self.timer:
422
+ self.timer.stop()
423
+ self.timer = None
424
+ self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
425
+
426
+ def _pulse(self):
427
+ self.frame = (self.frame + 1) % len(SMALL_SIGMA_FRAMES)
428
+ self.update(Text.from_markup(SMALL_SIGMA_FRAMES[self.frame]))
429
+
430
+
431
+ class TickerHighlight(Static):
432
+ """Display detected tickers in real-time."""
433
+
434
+ def update_tickers(self, text: str):
435
+ """Update displayed tickers based on input."""
436
+ tickers = extract_tickers(text)
437
+ if tickers:
438
+ ticker_text = " ".join([f"[cyan]${t}[/cyan]" for t in tickers[:3]])
439
+ self.update(Text.from_markup(ticker_text))
440
+ else:
441
+ self.update("")
442
+
443
+
444
+ class ChatLog(RichLog):
445
+ """Chat log with rich formatting."""
446
+
447
+ def write_user(self, message: str):
448
+ # Highlight any tickers in user message
449
+ highlighted = message
450
+ for ticker in extract_tickers(message):
451
+ highlighted = re.sub(
452
+ rf'\b{ticker}\b',
453
+ f'[cyan]{ticker}[/cyan]',
454
+ highlighted,
455
+ flags=re.IGNORECASE
456
+ )
457
+
458
+ self.write(Panel(
459
+ Text.from_markup(highlighted) if '[cyan]' in highlighted else Text(message, style="white"),
460
+ title="[bold blue]You[/bold blue]",
461
+ border_style="blue",
462
+ padding=(0, 1),
463
+ ))
464
+
465
+ def write_assistant(self, message: str):
466
+ self.write(Panel(
467
+ Markdown(message),
468
+ title=f"[bold cyan]{SIGMA} Sigma[/bold cyan]",
469
+ border_style="cyan",
470
+ padding=(0, 1),
471
+ ))
472
+
473
+ def write_tool(self, tool_name: str):
474
+ # This is now handled by ToolCallDisplay
475
+ pass
354
476
 
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}"
477
+ def write_error(self, message: str):
478
+ self.write(Panel(Text(message, style="red"), title="[red]Error[/red]", border_style="red"))
479
+
480
+ def write_system(self, message: str):
481
+ self.write(Text.from_markup(f"[dim]{message}[/dim]"))
482
+
483
+ def write_welcome(self):
484
+ self.write(Text.from_markup(WELCOME_BANNER))
485
+
486
+
487
+ class SigmaApp(App):
488
+ """Sigma Finance Research Agent."""
489
+
490
+ TITLE = "Sigma"
491
+ CSS = CSS
492
+
493
+ BINDINGS = [
494
+ Binding("ctrl+l", "clear", "Clear"),
495
+ Binding("ctrl+m", "models", "Models"),
496
+ Binding("ctrl+h", "help_toggle", "Help"),
497
+ Binding("ctrl+p", "palette", "palette", show=True),
498
+ Binding("escape", "cancel", show=False),
499
+ ]
500
+
501
+ def __init__(self):
502
+ super().__init__()
503
+ self.settings = get_settings()
504
+ self.llm = None
505
+ self.conversation = []
506
+ self.is_processing = False
507
+ self.history: List[str] = []
508
+ self.history_idx = -1
509
+ self.show_help = False
510
+
511
+ def compose(self) -> ComposeResult:
512
+ yield Container(
513
+ ScrollableContainer(
514
+ ChatLog(id="chat-log", highlight=True, markup=True),
515
+ id="chat-area",
516
+ ),
517
+ ToolCallDisplay(id="tool-calls-display"),
518
+ Static(id="help-panel"),
519
+ Container(
520
+ Horizontal(
521
+ SigmaIndicator(id="sigma-indicator"),
522
+ Input(
523
+ placeholder="Ask about any stock, market, or strategy... (Tab to autocomplete)",
524
+ id="prompt-input",
525
+ suggester=SigmaSuggester(),
526
+ ),
527
+ TickerHighlight(id="ticker-highlight"),
528
+ id="input-row",
529
+ ),
530
+ id="input-area",
531
+ ),
532
+ id="main-container",
533
+ )
534
+ yield Footer()
535
+
536
+ def on_mount(self):
537
+ chat = self.query_one("#chat-log", ChatLog)
538
+ chat.write_welcome()
405
539
 
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)
540
+ provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
541
+ chat.write_system(f"{SIGMA} Provider: {provider} | Model: {self.settings.default_model}")
542
+ chat.write_system(f"{SIGMA} Type /help for commands • Ctrl+H for quick help • Tab to autocomplete")
543
+ chat.write_system("")
410
544
 
411
- return formatted
545
+ self._init_llm()
546
+ self.query_one("#prompt-input", Input).focus()
412
547
 
413
- def _print_table(self, lines: list[str]):
414
- """Print a markdown table as a Rich table."""
415
- if len(lines) < 2:
548
+ def _init_llm(self):
549
+ try:
550
+ self.llm = get_llm(self.settings.default_provider, self.settings.default_model)
551
+ except Exception as e:
552
+ chat = self.query_one("#chat-log", ChatLog)
553
+ chat.write_error(f"LLM init failed: {e}")
554
+ chat.write_system("Use /keys to configure API keys")
555
+
556
+ @on(Input.Changed)
557
+ def on_input_change(self, event: Input.Changed):
558
+ """Update ticker highlight as user types."""
559
+ ticker_display = self.query_one("#ticker-highlight", TickerHighlight)
560
+ ticker_display.update_tickers(event.value)
561
+
562
+ @on(Input.Submitted)
563
+ def handle_input(self, event: Input.Submitted):
564
+ if self.is_processing:
416
565
  return
417
566
 
418
- header_line = lines[0]
419
- headers = [h.strip() for h in header_line.split('|') if h.strip()]
567
+ text = event.value.strip()
568
+ if not text:
569
+ return
420
570
 
421
- table = Table(box=ROUNDED, show_header=True, header_style="bold bright_cyan")
422
- for h in headers:
423
- table.add_column(h)
571
+ self.query_one("#prompt-input", Input).value = ""
572
+ self.history.append(text)
573
+ self.history_idx = len(self.history)
424
574
 
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)
575
+ chat = self.query_one("#chat-log", ChatLog)
440
576
 
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
577
+ if text.startswith("/"):
578
+ self._handle_command(text, chat)
579
+ else:
580
+ chat.write_user(text)
581
+ self._process_query(text, chat)
582
+
583
+ def _handle_command(self, cmd: str, chat: ChatLog):
584
+ parts = cmd.lower().split()
585
+ command = parts[0]
586
+ args = parts[1:] if len(parts) > 1 else []
463
587
 
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()
588
+ if command == "/help":
589
+ self._show_comprehensive_help(chat)
590
+ elif command == "/clear":
591
+ chat.clear()
592
+ self.conversation = []
593
+ chat.write_system("Chat cleared")
594
+ elif command == "/keys":
595
+ self._show_keys(chat)
596
+ elif command == "/models":
597
+ self._show_models(chat)
598
+ elif command == "/status":
599
+ self._show_status(chat)
600
+ elif command == "/backtest":
601
+ self._show_strategies(chat)
602
+ elif command == "/provider" and args:
603
+ self._switch_provider(args[0], chat)
604
+ elif command == "/model" and args:
605
+ self._switch_model(args[0], chat)
606
+ elif command.startswith("/setkey") and len(parts) >= 3:
607
+ self._set_key(parts[1], parts[2], chat)
608
+ elif command == "/tickers":
609
+ self._show_popular_tickers(chat)
610
+ else:
611
+ chat.write_error(f"Unknown command: {command}. Type /help for available commands.")
612
+
613
+ def _show_comprehensive_help(self, chat: ChatLog):
614
+ """Show comprehensive help with examples."""
615
+ help_text = f"""
616
+ [bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
617
+ [bold] {SIGMA} SIGMA HELP CENTER [/bold]
618
+ [bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
619
+
620
+ [bold yellow]QUICK START[/bold yellow]
621
+ Just type naturally! Examples:
622
+ • "analyze AAPL" - Full analysis of Apple
623
+ • "compare NVDA AMD INTC" - Compare multiple stocks
624
+ • "is TSLA overvalued?" - Get AI insights
625
+ • "market overview" - See major indices
626
+
627
+ [bold yellow]COMMANDS[/bold yellow]
628
+ [cyan]/help[/cyan] This help screen
629
+ [cyan]/clear[/cyan] Clear chat history
630
+ [cyan]/keys[/cyan] Configure API keys
631
+ [cyan]/models[/cyan] Show available models
632
+ [cyan]/status[/cyan] Current configuration
633
+ [cyan]/backtest[/cyan] Show backtest strategies
634
+ [cyan]/provider <name>[/cyan] Switch AI provider
635
+ [cyan]/model <name>[/cyan] Switch model
636
+ [cyan]/setkey <p> <k>[/cyan] Set API key
637
+ [cyan]/tickers[/cyan] Popular tickers list
638
+
639
+ [bold yellow]ANALYSIS EXAMPLES[/bold yellow]
640
+ • "technical analysis of SPY"
641
+ • "fundamentals of MSFT"
642
+ • "insider trading for AAPL"
643
+ • "analyst recommendations for NVDA"
644
+ • "sector performance"
645
+
646
+ [bold yellow]BACKTESTING[/bold yellow]
647
+ • "backtest SMA crossover on AAPL"
648
+ • "backtest RSI strategy on SPY"
649
+ • "backtest MACD on NVDA"
650
+ Strategies: sma_crossover, rsi, macd, bollinger, momentum, breakout
651
+
652
+ [bold yellow]KEYBOARD SHORTCUTS[/bold yellow]
653
+ [bold]Tab[/bold] Autocomplete suggestion
654
+ [bold]Ctrl+L[/bold] Clear chat
655
+ [bold]Ctrl+M[/bold] Show models
656
+ [bold]Ctrl+H[/bold] Toggle quick help
657
+ [bold]Ctrl+P[/bold] Command palette
658
+ [bold]Esc[/bold] Cancel operation
659
+
660
+ [bold yellow]TIPS[/bold yellow]
661
+ • Type [cyan]$AAPL[/cyan] or [cyan]AAPL[/cyan] - tickers auto-detected
662
+ • Use Tab for smart autocomplete
663
+ • Detected tickers shown next to input
664
+ """
665
+ chat.write(Panel(
666
+ Text.from_markup(help_text),
667
+ title=f"[bold cyan]{SIGMA} Help[/bold cyan]",
668
+ border_style="cyan",
669
+ padding=(0, 1),
670
+ ))
671
+
672
+ def _show_popular_tickers(self, chat: ChatLog):
673
+ """Show popular tickers organized by category."""
674
+ tickers_text = """
675
+ [bold]Tech Giants[/bold]: AAPL, MSFT, GOOGL, AMZN, META, NVDA
676
+ [bold]Semiconductors[/bold]: NVDA, AMD, INTC, AVGO, QCOM, TSM
677
+ [bold]EVs & Auto[/bold]: TSLA, RIVN, LCID, F, GM
678
+ [bold]Finance[/bold]: JPM, BAC, GS, MS, V, MA
679
+ [bold]Healthcare[/bold]: JNJ, PFE, UNH, MRK, ABBV
680
+ [bold]ETFs[/bold]: SPY, QQQ, IWM, DIA, VTI, VOO
681
+ [bold]Sector ETFs[/bold]: XLK, XLF, XLE, XLV, XLI
682
+ """
683
+ chat.write(Panel(
684
+ Text.from_markup(tickers_text),
685
+ title=f"[cyan]{SIGMA} Popular Tickers[/cyan]",
686
+ border_style="dim",
687
+ ))
688
+
689
+ def _show_keys(self, chat: ChatLog):
690
+ chat.write_system(f"""
691
+ [bold]{SIGMA} API Keys[/bold]
692
+ Set key: /setkey <provider> <key>
693
+
694
+ Providers: google, openai, anthropic, groq, xai
695
+ Example: /setkey google AIzaSy...
696
+ """)
697
+ self._show_status(chat)
698
+
699
+ def _show_status(self, chat: ChatLog):
700
+ table = Table(show_header=False, box=None, padding=(0, 2))
701
+ table.add_column("", style="bold")
702
+ table.add_column("")
703
+
704
+ provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
705
+ table.add_row("Provider", provider)
706
+ table.add_row("Model", self.settings.default_model)
707
+ table.add_row("", "")
708
+
709
+ keys = [
710
+ ("Google", self.settings.google_api_key),
711
+ ("OpenAI", self.settings.openai_api_key),
712
+ ("Anthropic", self.settings.anthropic_api_key),
713
+ ("Groq", self.settings.groq_api_key),
714
+ ("xAI", self.settings.xai_api_key),
715
+ ]
716
+ for name, key in keys:
717
+ status = "[green]OK[/green]" if key else "[dim]--[/dim]"
718
+ table.add_row(f" {name}", Text.from_markup(status))
719
+
720
+ chat.write(Panel(table, title=f"[cyan]{SIGMA} Config[/cyan]", border_style="dim"))
721
+
722
+ def _show_models(self, chat: ChatLog):
723
+ table = Table(title=f"{SIGMA} Models", show_header=True, border_style="dim")
724
+ table.add_column("Provider", style="cyan")
725
+ table.add_column("Models")
726
+ for p, m in AVAILABLE_MODELS.items():
727
+ table.add_row(p, ", ".join(m))
728
+ chat.write(table)
729
+
730
+ def _show_strategies(self, chat: ChatLog):
731
+ strategies = get_available_strategies()
732
+ table = Table(title=f"{SIGMA} Strategies", show_header=True, border_style="dim")
733
+ table.add_column("Name", style="cyan")
734
+ table.add_column("Description")
735
+ for k, v in strategies.items():
736
+ table.add_row(k, v.get('description', ''))
737
+ chat.write(table)
738
+
739
+ def _switch_provider(self, provider: str, chat: ChatLog):
740
+ valid = ["google", "openai", "anthropic", "groq", "xai", "ollama"]
741
+ if provider not in valid:
742
+ chat.write_error(f"Invalid. Use: {', '.join(valid)}")
491
743
  return
492
-
493
- subcmd = args[0].lower()
494
-
495
- if subcmd == "help":
496
- self._handle_lean_command([])
744
+ try:
745
+ self.settings.default_provider = LLMProvider(provider)
746
+ if provider in AVAILABLE_MODELS:
747
+ self.settings.default_model = AVAILABLE_MODELS[provider][0]
748
+ self._init_llm()
749
+ chat.write_system(f"Switched to {provider}")
750
+ except Exception as e:
751
+ chat.write_error(str(e))
752
+
753
+ def _switch_model(self, model: str, chat: ChatLog):
754
+ self.settings.default_model = model
755
+ self._init_llm()
756
+ chat.write_system(f"Model: {model}")
757
+
758
+ def _set_key(self, provider: str, key: str, chat: ChatLog):
759
+ try:
760
+ save_api_key(LLMProvider(provider), key)
761
+ chat.write_system(f"{SIGMA} Key saved for {provider}")
762
+ if provider == getattr(self.settings.default_provider, 'value', ''):
763
+ self._init_llm()
764
+ except Exception as e:
765
+ chat.write_error(str(e))
766
+
767
+ @work(exclusive=True)
768
+ async def _process_query(self, query: str, chat: ChatLog):
769
+ if not self.llm:
770
+ chat.write_error("No LLM. Use /keys to configure.")
497
771
  return
498
772
 
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
773
+ self.is_processing = True
774
+ indicator = self.query_one("#sigma-indicator", SigmaIndicator)
775
+ tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
776
+ ticker_highlight = self.query_one("#ticker-highlight", TickerHighlight)
515
777
 
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()
778
+ # Clear ticker highlight and start sigma animation
779
+ ticker_highlight.update("")
780
+ indicator.set_active(True)
561
781
 
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()
782
+ try:
783
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
784
+ messages.extend(self.conversation)
785
+ messages.append({"role": "user", "content": query})
574
786
 
575
- result = run_lean_backtest(symbol, strategy)
787
+ all_tools = TOOLS + [BACKTEST_TOOL]
576
788
 
577
- for step in result["steps"]:
578
- console.print(f" [dim]>[/dim] {step}")
789
+ async def on_tool(name: str, args: dict):
790
+ tool_display.add_tool_call(name)
791
+ if name == "run_backtest":
792
+ result = run_backtest(**args)
793
+ else:
794
+ result = execute_tool(name, args)
795
+ tool_display.complete_tool_call(name)
796
+ return result
579
797
 
580
- console.print()
798
+ response = await self.llm.generate(messages, tools=all_tools, on_tool_call=on_tool)
581
799
 
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
- ))
800
+ # Clear tool display after getting response
801
+ await asyncio.sleep(0.5) # Brief pause to show completion
802
+ tool_display.clear()
703
803
 
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")
804
+ if response:
805
+ chat.write_assistant(response)
806
+ self.conversation.append({"role": "user", "content": query})
807
+ self.conversation.append({"role": "assistant", "content": response})
808
+ if len(self.conversation) > 20:
809
+ self.conversation = self.conversation[-20:]
778
810
  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
-
811
+ chat.write_error("No response")
812
+ except Exception as e:
813
+ tool_display.clear()
814
+ chat.write_error(str(e))
815
+ finally:
816
+ indicator.set_active(False)
817
+ self.is_processing = False
818
+ self.query_one("#prompt-input", Input).focus()
819
+
820
+ def action_clear(self):
821
+ chat = self.query_one("#chat-log", ChatLog)
822
+ chat.clear()
823
+ self.conversation = []
824
+ chat.write_system("Cleared")
825
+
826
+ def action_models(self):
827
+ self._show_models(self.query_one("#chat-log", ChatLog))
828
+
829
+ def action_help_toggle(self):
830
+ """Toggle quick help panel."""
831
+ help_panel = self.query_one("#help-panel", Static)
832
+ if self.show_help:
833
+ help_panel.remove_class("visible")
834
+ help_panel.update("")
816
835
  else:
817
- console.print(f"\n [red]Unknown command:[/red] {command}. Type /help for commands.\n")
818
-
819
- return True
836
+ help_panel.add_class("visible")
837
+ help_panel.update(Text.from_markup(
838
+ "[bold]Quick Commands:[/bold] /help /clear /keys /models /status /backtest "
839
+ "[bold]Shortcuts:[/bold] Tab=autocomplete Ctrl+L=clear Ctrl+M=models"
840
+ ))
841
+ self.show_help = not self.show_help
820
842
 
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")
843
+ def action_cancel(self):
844
+ if self.is_processing:
845
+ self.is_processing = False
846
+ tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
847
+ tool_display.clear()
848
+
849
+
850
+ def launch():
851
+ """Launch Sigma."""
852
+ SigmaApp().run()
944
853
 
945
854
 
946
855
  if __name__ == "__main__":
947
- main()
856
+ launch()