sigma-terminal 3.2.0__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,7 +1,8 @@
1
- """Sigma v3.2.0 - Finance Research Agent."""
1
+ """Sigma v3.3.0 - Finance Research Agent."""
2
2
 
3
3
  import asyncio
4
4
  import os
5
+ import re
5
6
  from datetime import datetime
6
7
  from typing import Optional, List
7
8
 
@@ -12,7 +13,7 @@ from rich.text import Text
12
13
  from textual import on, work
13
14
  from textual.app import App, ComposeResult
14
15
  from textual.binding import Binding
15
- from textual.containers import Container, Horizontal, ScrollableContainer
16
+ from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
16
17
  from textual.widgets import Footer, Input, RichLog, Static
17
18
  from textual.suggester import Suggester
18
19
 
@@ -22,43 +23,32 @@ from .tools import TOOLS, execute_tool
22
23
  from .backtest import run_backtest, get_available_strategies, BACKTEST_TOOL
23
24
 
24
25
 
25
- __version__ = "3.2.0"
26
+ __version__ = "3.3.0"
26
27
  SIGMA = "σ"
27
28
 
28
- # Animated sigma logo frames for thinking state
29
- THINKING_LOGO_FRAMES = [
30
- """[bold blue]
31
- ███████╗██╗ ██████╗ ███╗ ███╗ █████╗
32
- ██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
33
- ███████╗██║██║ ███╗██╔████╔██║███████║
34
- ╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
35
- ███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
36
- ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold blue]
37
- [dim]analyzing[/dim]""",
38
- """[bold cyan]
39
- ███████╗██╗ ██████╗ ███╗ ███╗ █████╗
40
- ██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
41
- ███████╗██║██║ ███╗██╔████╔██║███████║
42
- ╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
43
- ███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
44
- ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold cyan]
45
- [dim]analyzing.[/dim]""",
46
- """[bold white]
47
- ███████╗██╗ ██████╗ ███╗ ███╗ █████╗
48
- ██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
49
- ███████╗██║██║ ███╗██╔████╔██║███████║
50
- ╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
51
- ███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
52
- ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold white]
53
- [dim]analyzing..[/dim]""",
54
- """[bold cyan]
55
- ███████╗██╗ ██████╗ ███╗ ███╗ █████╗
56
- ██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
57
- ███████╗██║██║ ███╗██╔████╔██║███████║
58
- ╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
59
- ███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
60
- ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold cyan]
61
- [dim]analyzing...[/dim]""",
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
+ ]
48
+
49
+ # Tool call animation frames
50
+ TOOL_CALL_FRAMES = [
51
+ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
62
52
  ]
63
53
 
64
54
  # Welcome banner - clean design
@@ -70,7 +60,7 @@ WELCOME_BANNER = """
70
60
  [bold blue]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold blue]
71
61
  [bold blue]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold blue]
72
62
 
73
- [bold cyan]Finance Research Agent[/bold cyan] [dim]v3.2.0 | Native macOS[/dim]
63
+ [bold cyan]Finance Research Agent[/bold cyan] [dim]v3.3.0 | Native macOS[/dim]
74
64
  """
75
65
 
76
66
  SYSTEM_PROMPT = """You are Sigma, a Finance Research Agent. You provide comprehensive market analysis, trading strategies, and investment insights.
@@ -95,8 +85,9 @@ RESPONSE STYLE:
95
85
 
96
86
  When users ask about stocks, always gather current data using your tools before responding."""
97
87
 
98
- # Autocomplete suggestions
88
+ # Enhanced autocomplete suggestions with more variety
99
89
  SUGGESTIONS = [
90
+ # Analysis commands
100
91
  "analyze AAPL",
101
92
  "analyze MSFT",
102
93
  "analyze GOOGL",
@@ -104,30 +95,79 @@ SUGGESTIONS = [
104
95
  "analyze TSLA",
105
96
  "analyze META",
106
97
  "analyze AMZN",
98
+ "analyze AMD",
99
+ "analyze SPY",
100
+ # Comparisons
107
101
  "compare AAPL MSFT GOOGL",
108
102
  "compare NVDA AMD INTC",
103
+ "compare META GOOGL AMZN",
104
+ "compare TSLA RIVN LCID",
105
+ # Technical
109
106
  "technical analysis of AAPL",
110
107
  "technical analysis of SPY",
108
+ "technical analysis of NVDA",
109
+ "technical analysis of QQQ",
110
+ # Backtesting
111
111
  "backtest SMA crossover on AAPL",
112
112
  "backtest RSI strategy on SPY",
113
+ "backtest MACD on NVDA",
114
+ "backtest momentum on QQQ",
115
+ # Market
113
116
  "market overview",
114
117
  "sector performance",
118
+ "what sectors are hot today",
119
+ # Quotes
115
120
  "get quote for AAPL",
116
121
  "price of NVDA",
122
+ "how is TSLA doing",
123
+ # Fundamentals
117
124
  "fundamentals of MSFT",
118
- "insider trading activity",
119
- "institutional holders",
120
- "analyst recommendations",
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
121
137
  "/help",
122
138
  "/clear",
123
139
  "/keys",
124
140
  "/models",
125
141
  "/status",
142
+ "/backtest",
126
143
  ]
127
144
 
128
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
+
129
169
  class SigmaSuggester(Suggester):
130
- """Autocomplete suggester for Sigma."""
170
+ """Enhanced autocomplete suggester with ticker recognition."""
131
171
 
132
172
  def __init__(self):
133
173
  super().__init__(use_cache=True, case_sensitive=False)
@@ -138,9 +178,24 @@ class SigmaSuggester(Suggester):
138
178
  return None
139
179
 
140
180
  value_lower = value.lower()
181
+
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}"
188
+
189
+ # Standard suggestions
141
190
  for suggestion in SUGGESTIONS:
142
191
  if suggestion.lower().startswith(value_lower):
143
192
  return suggestion
193
+
194
+ # Try partial match in middle of suggestion
195
+ for suggestion in SUGGESTIONS:
196
+ if value_lower in suggestion.lower():
197
+ return suggestion
198
+
144
199
  return None
145
200
 
146
201
 
@@ -173,16 +228,43 @@ Screen {
173
228
  padding: 1 0;
174
229
  }
175
230
 
176
- #thinking-area {
177
- height: auto;
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 {
178
240
  width: 100%;
241
+ height: 100%;
242
+ content-align: left middle;
243
+ }
244
+
245
+ #thinking-indicator {
246
+ width: auto;
247
+ height: 1;
179
248
  content-align: center middle;
180
- background: #0a0a0f;
181
249
  display: none;
182
- padding: 1;
183
250
  }
184
251
 
185
- #thinking-area.visible {
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 {
186
268
  display: block;
187
269
  }
188
270
 
@@ -219,6 +301,13 @@ Screen {
219
301
  border: solid #22c55e;
220
302
  }
221
303
 
304
+ #ticker-highlight {
305
+ width: auto;
306
+ height: 1;
307
+ padding: 0 1;
308
+ background: transparent;
309
+ }
310
+
222
311
  Footer {
223
312
  background: #0d1117;
224
313
  height: 1;
@@ -238,26 +327,51 @@ Footer > .footer--key {
238
327
  Footer > .footer--description {
239
328
  color: #6b7280;
240
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
+ }
241
344
  """
242
345
 
243
346
 
244
- class ThinkingDisplay(Static):
245
- """Animated sigma logo during thinking."""
347
+ class ToolCallDisplay(Static):
348
+ """Animated display for tool calls."""
246
349
 
247
350
  def __init__(self, *args, **kwargs):
248
351
  super().__init__(*args, **kwargs)
352
+ self.tool_calls: List[dict] = []
249
353
  self.frame = 0
250
354
  self.timer = None
251
355
 
252
- def start(self):
253
- """Start the animation."""
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})
254
359
  self.add_class("visible")
255
- self.frame = 0
256
- self.update(Text.from_markup(THINKING_LOGO_FRAMES[0]))
257
- self.timer = self.set_interval(0.3, self._animate)
258
-
259
- def stop(self):
260
- """Stop the animation."""
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()
371
+
372
+ def clear(self):
373
+ """Clear all tool calls."""
374
+ self.tool_calls = []
261
375
  if self.timer:
262
376
  self.timer.stop()
263
377
  self.timer = None
@@ -265,13 +379,31 @@ class ThinkingDisplay(Static):
265
379
  self.update("")
266
380
 
267
381
  def _animate(self):
268
- """Cycle through animation frames."""
269
- self.frame = (self.frame + 1) % len(THINKING_LOGO_FRAMES)
270
- self.update(Text.from_markup(THINKING_LOGO_FRAMES[self.frame]))
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
393
+
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]")
401
+
402
+ self.update(Text.from_markup("\n".join(lines)))
271
403
 
272
404
 
273
405
  class SigmaIndicator(Static):
274
- """Pulsing sigma indicator."""
406
+ """Pulsing sigma indicator with minimal footprint."""
275
407
 
276
408
  def __init__(self, *args, **kwargs):
277
409
  super().__init__(*args, **kwargs)
@@ -285,24 +417,46 @@ class SigmaIndicator(Static):
285
417
  def set_active(self, active: bool):
286
418
  self.active = active
287
419
  if active and not self.timer:
288
- self.timer = self.set_interval(0.2, self._pulse)
420
+ self.timer = self.set_interval(0.15, self._pulse)
289
421
  elif not active and self.timer:
290
422
  self.timer.stop()
291
423
  self.timer = None
292
424
  self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
293
425
 
294
426
  def _pulse(self):
295
- colors = ["#3b82f6", "#60a5fa", "#93c5fd", "#60a5fa"]
296
- self.frame = (self.frame + 1) % len(colors)
297
- self.update(Text.from_markup(f"[bold {colors[self.frame]}]{SIGMA}[/bold {colors[self.frame]}]"))
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("")
298
442
 
299
443
 
300
444
  class ChatLog(RichLog):
301
445
  """Chat log with rich formatting."""
302
446
 
303
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
+
304
458
  self.write(Panel(
305
- Text(message, style="white"),
459
+ Text.from_markup(highlighted) if '[cyan]' in highlighted else Text(message, style="white"),
306
460
  title="[bold blue]You[/bold blue]",
307
461
  border_style="blue",
308
462
  padding=(0, 1),
@@ -317,7 +471,8 @@ class ChatLog(RichLog):
317
471
  ))
318
472
 
319
473
  def write_tool(self, tool_name: str):
320
- self.write(Text.from_markup(f" [dim]{SIGMA} executing {tool_name}...[/dim]"))
474
+ # This is now handled by ToolCallDisplay
475
+ pass
321
476
 
322
477
  def write_error(self, message: str):
323
478
  self.write(Panel(Text(message, style="red"), title="[red]Error[/red]", border_style="red"))
@@ -338,6 +493,7 @@ class SigmaApp(App):
338
493
  BINDINGS = [
339
494
  Binding("ctrl+l", "clear", "Clear"),
340
495
  Binding("ctrl+m", "models", "Models"),
496
+ Binding("ctrl+h", "help_toggle", "Help"),
341
497
  Binding("ctrl+p", "palette", "palette", show=True),
342
498
  Binding("escape", "cancel", show=False),
343
499
  ]
@@ -350,6 +506,7 @@ class SigmaApp(App):
350
506
  self.is_processing = False
351
507
  self.history: List[str] = []
352
508
  self.history_idx = -1
509
+ self.show_help = False
353
510
 
354
511
  def compose(self) -> ComposeResult:
355
512
  yield Container(
@@ -357,15 +514,17 @@ class SigmaApp(App):
357
514
  ChatLog(id="chat-log", highlight=True, markup=True),
358
515
  id="chat-area",
359
516
  ),
360
- Static(id="thinking-area"),
517
+ ToolCallDisplay(id="tool-calls-display"),
518
+ Static(id="help-panel"),
361
519
  Container(
362
520
  Horizontal(
363
521
  SigmaIndicator(id="sigma-indicator"),
364
522
  Input(
365
- placeholder="Ask about any stock, market, or strategy...",
523
+ placeholder="Ask about any stock, market, or strategy... (Tab to autocomplete)",
366
524
  id="prompt-input",
367
525
  suggester=SigmaSuggester(),
368
526
  ),
527
+ TickerHighlight(id="ticker-highlight"),
369
528
  id="input-row",
370
529
  ),
371
530
  id="input-area",
@@ -380,7 +539,7 @@ class SigmaApp(App):
380
539
 
381
540
  provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
382
541
  chat.write_system(f"{SIGMA} Provider: {provider} | Model: {self.settings.default_model}")
383
- chat.write_system(f"{SIGMA} Type /help for commands or ask anything about markets")
542
+ chat.write_system(f"{SIGMA} Type /help for commands Ctrl+H for quick help • Tab to autocomplete")
384
543
  chat.write_system("")
385
544
 
386
545
  self._init_llm()
@@ -394,6 +553,12 @@ class SigmaApp(App):
394
553
  chat.write_error(f"LLM init failed: {e}")
395
554
  chat.write_system("Use /keys to configure API keys")
396
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
+
397
562
  @on(Input.Submitted)
398
563
  def handle_input(self, event: Input.Submitted):
399
564
  if self.is_processing:
@@ -421,20 +586,7 @@ class SigmaApp(App):
421
586
  args = parts[1:] if len(parts) > 1 else []
422
587
 
423
588
  if command == "/help":
424
- chat.write_system(f"""
425
- [bold]{SIGMA} Commands[/bold]
426
- /help Show commands
427
- /clear Clear chat
428
- /keys Configure API keys
429
- /models Show models
430
- /provider <name> Switch provider
431
- /model <name> Switch model
432
- /status Show configuration
433
- /backtest Show strategies
434
-
435
- [bold]{SIGMA} Shortcuts[/bold]
436
- Ctrl+L Clear Ctrl+M Models Ctrl+P Palette
437
- """)
589
+ self._show_comprehensive_help(chat)
438
590
  elif command == "/clear":
439
591
  chat.clear()
440
592
  self.conversation = []
@@ -453,8 +605,86 @@ class SigmaApp(App):
453
605
  self._switch_model(args[0], chat)
454
606
  elif command.startswith("/setkey") and len(parts) >= 3:
455
607
  self._set_key(parts[1], parts[2], chat)
608
+ elif command == "/tickers":
609
+ self._show_popular_tickers(chat)
456
610
  else:
457
- chat.write_error(f"Unknown command: {command}")
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
+ ))
458
688
 
459
689
  def _show_keys(self, chat: ChatLog):
460
690
  chat.write_system(f"""
@@ -541,19 +771,12 @@ Example: /setkey google AIzaSy...
541
771
  return
542
772
 
543
773
  self.is_processing = True
544
- thinking = self.query_one("#thinking-area", Static)
545
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)
546
777
 
547
- # Start animated thinking display
548
- thinking.add_class("visible")
549
- frame = [0]
550
-
551
- def animate():
552
- thinking.update(Text.from_markup(THINKING_LOGO_FRAMES[frame[0]]))
553
- frame[0] = (frame[0] + 1) % len(THINKING_LOGO_FRAMES)
554
-
555
- animate()
556
- timer = self.set_interval(0.3, animate)
778
+ # Clear ticker highlight and start sigma animation
779
+ ticker_highlight.update("")
557
780
  indicator.set_active(True)
558
781
 
559
782
  try:
@@ -564,13 +787,20 @@ Example: /setkey google AIzaSy...
564
787
  all_tools = TOOLS + [BACKTEST_TOOL]
565
788
 
566
789
  async def on_tool(name: str, args: dict):
567
- chat.write_tool(name)
790
+ tool_display.add_tool_call(name)
568
791
  if name == "run_backtest":
569
- return run_backtest(**args)
570
- return execute_tool(name, args)
792
+ result = run_backtest(**args)
793
+ else:
794
+ result = execute_tool(name, args)
795
+ tool_display.complete_tool_call(name)
796
+ return result
571
797
 
572
798
  response = await self.llm.generate(messages, tools=all_tools, on_tool_call=on_tool)
573
799
 
800
+ # Clear tool display after getting response
801
+ await asyncio.sleep(0.5) # Brief pause to show completion
802
+ tool_display.clear()
803
+
574
804
  if response:
575
805
  chat.write_assistant(response)
576
806
  self.conversation.append({"role": "user", "content": query})
@@ -580,11 +810,9 @@ Example: /setkey google AIzaSy...
580
810
  else:
581
811
  chat.write_error("No response")
582
812
  except Exception as e:
813
+ tool_display.clear()
583
814
  chat.write_error(str(e))
584
815
  finally:
585
- timer.stop()
586
- thinking.remove_class("visible")
587
- thinking.update("")
588
816
  indicator.set_active(False)
589
817
  self.is_processing = False
590
818
  self.query_one("#prompt-input", Input).focus()
@@ -598,9 +826,25 @@ Example: /setkey google AIzaSy...
598
826
  def action_models(self):
599
827
  self._show_models(self.query_one("#chat-log", ChatLog))
600
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("")
835
+ else:
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
842
+
601
843
  def action_cancel(self):
602
844
  if self.is_processing:
603
845
  self.is_processing = False
846
+ tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
847
+ tool_display.clear()
604
848
 
605
849
 
606
850
  def launch():