sigma-terminal 3.4.0__py3-none-any.whl → 3.5.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 +4 -5
- sigma/analytics/__init__.py +11 -9
- sigma/app.py +384 -1125
- sigma/backtest/__init__.py +2 -0
- sigma/backtest/service.py +116 -0
- sigma/charts.py +2 -2
- sigma/cli.py +15 -13
- sigma/comparison.py +2 -2
- sigma/config.py +25 -12
- sigma/core/command_router.py +93 -0
- sigma/llm/__init__.py +3 -0
- sigma/llm/providers/anthropic_provider.py +196 -0
- sigma/llm/providers/base.py +29 -0
- sigma/llm/providers/google_provider.py +197 -0
- sigma/llm/providers/ollama_provider.py +156 -0
- sigma/llm/providers/openai_provider.py +168 -0
- sigma/llm/providers/sigma_cloud_provider.py +57 -0
- sigma/llm/rate_limit.py +40 -0
- sigma/llm/registry.py +66 -0
- sigma/llm/router.py +122 -0
- sigma/setup_agent.py +188 -0
- sigma/tools/__init__.py +23 -0
- sigma/tools/adapter.py +38 -0
- sigma/{tools.py → tools/library.py} +593 -1
- sigma/tools/registry.py +108 -0
- sigma/utils/extraction.py +83 -0
- sigma_terminal-3.5.0.dist-info/METADATA +184 -0
- sigma_terminal-3.5.0.dist-info/RECORD +46 -0
- sigma/llm.py +0 -786
- sigma/setup.py +0 -440
- sigma_terminal-3.4.0.dist-info/METADATA +0 -264
- sigma_terminal-3.4.0.dist-info/RECORD +0 -30
- /sigma/{backtest.py → backtest/simple_engine.py} +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/WHEEL +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/entry_points.txt +0 -0
- {sigma_terminal-3.4.0.dist-info → sigma_terminal-3.5.0.dist-info}/licenses/LICENSE +0 -0
sigma/app.py
CHANGED
|
@@ -1,1186 +1,445 @@
|
|
|
1
|
-
"""Sigma v3.
|
|
1
|
+
"""Sigma v3.5.0 - Finance Research Agent."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
4
|
from datetime import datetime
|
|
7
|
-
from typing import
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import re # Added top-level import
|
|
8
8
|
|
|
9
9
|
from rich.markdown import Markdown
|
|
10
|
-
from rich.
|
|
10
|
+
from rich.markup import escape
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
from rich.text import Text
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.align import Align
|
|
15
|
+
|
|
13
16
|
from textual import on, work
|
|
14
17
|
from textual.app import App, ComposeResult
|
|
15
18
|
from textual.binding import Binding
|
|
16
|
-
from textual.containers import
|
|
17
|
-
from textual.
|
|
18
|
-
from textual.
|
|
19
|
-
|
|
20
|
-
from .
|
|
21
|
-
|
|
22
|
-
from .
|
|
23
|
-
from .
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Sigma animation frames - color cycling for thinking/processing state
|
|
42
|
-
SIGMA_FRAMES = [
|
|
43
|
-
"[bold #3b82f6]s[/bold #3b82f6]",
|
|
44
|
-
"[bold #60a5fa]si[/bold #60a5fa]",
|
|
45
|
-
"[bold #93c5fd]sig[/bold #93c5fd]",
|
|
46
|
-
"[bold #bfdbfe]sigm[/bold #bfdbfe]",
|
|
47
|
-
"[bold white]sigma[/bold white]",
|
|
48
|
-
"[bold #bfdbfe]sigm[/bold #bfdbfe]",
|
|
49
|
-
"[bold #93c5fd]sig[/bold #93c5fd]",
|
|
50
|
-
"[bold #60a5fa]si[/bold #60a5fa]",
|
|
51
|
-
"[bold #3b82f6]s[/bold #3b82f6]",
|
|
52
|
-
"[bold cyan].[/bold cyan]",
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
# Sigma pulse animation (color breathing)
|
|
56
|
-
SIGMA_PULSE_FRAMES = [
|
|
57
|
-
"[bold #1e40af]o[/bold #1e40af]",
|
|
58
|
-
"[bold #2563eb]o[/bold #2563eb]",
|
|
59
|
-
"[bold #3b82f6]o[/bold #3b82f6]",
|
|
60
|
-
"[bold #60a5fa]o[/bold #60a5fa]",
|
|
61
|
-
"[bold #93c5fd]o[/bold #93c5fd]",
|
|
62
|
-
"[bold #bfdbfe]o[/bold #bfdbfe]",
|
|
63
|
-
"[bold #93c5fd]o[/bold #93c5fd]",
|
|
64
|
-
"[bold #60a5fa]o[/bold #60a5fa]",
|
|
65
|
-
"[bold #3b82f6]o[/bold #3b82f6]",
|
|
66
|
-
"[bold #2563eb]o[/bold #2563eb]",
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
# Tool call spinner frames - ASCII based
|
|
70
|
-
TOOL_SPINNER_FRAMES = [
|
|
71
|
-
"|", "/", "-", "\\"
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
# Welcome banner - clean design
|
|
75
|
-
WELCOME_BANNER = """
|
|
76
|
-
[bold #3b82f6]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold #3b82f6]
|
|
77
|
-
[bold #60a5fa]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold #60a5fa]
|
|
78
|
-
[bold #93c5fd]███████╗██║██║ ███╗██╔████╔██║███████║[/bold #93c5fd]
|
|
79
|
-
[bold #60a5fa]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold #60a5fa]
|
|
80
|
-
[bold #3b82f6]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold #3b82f6]
|
|
81
|
-
[bold #1d4ed8]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold #1d4ed8]
|
|
82
|
-
|
|
83
|
-
[bold cyan]Finance Research Agent[/bold cyan] [dim]v3.4.0[/dim]
|
|
19
|
+
from textual.containers import Vertical, Horizontal, Container
|
|
20
|
+
from textual.screen import Screen
|
|
21
|
+
from textual.widgets import Input, RichLog, Label, Static, LoadingIndicator
|
|
22
|
+
from textual.reactive import reactive
|
|
23
|
+
from textual.suggester import SuggestFromList
|
|
24
|
+
|
|
25
|
+
from .config import get_settings
|
|
26
|
+
from .core.command_router import CommandRouter, Request
|
|
27
|
+
from .llm.router import get_router
|
|
28
|
+
from .tools.registry import TOOL_REGISTRY
|
|
29
|
+
|
|
30
|
+
__version__ = "3.5.0"
|
|
31
|
+
|
|
32
|
+
# -----------------------------------------------------------------------------
|
|
33
|
+
# CONSTANTS & ASSETS
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
SIGMA_ASCII = """
|
|
37
|
+
[#d97757] ██████ ██[/] [#e08e79]██████ ███ ███ █████ [/]
|
|
38
|
+
[#d97757] ██ ██[/] [#e08e79]██ ████ ████ ██ ██[/]
|
|
39
|
+
[#d97757] ███████ ██[/] [#e08e79]██ ███ ██ ████ ██ ███████[/]
|
|
40
|
+
[#d97757] ██ ██[/] [#e08e79]██ ██ ██ ██ ██ ██ ██[/]
|
|
41
|
+
[#d97757] ██████ ██[/] [#e08e79] ██████ ██ ██ ██ ██[/]
|
|
84
42
|
"""
|
|
85
43
|
|
|
86
|
-
SYSTEM_PROMPT = """You are Sigma (σ), an elite AI-powered Finance Research Agent. You combine the analytical rigor of a quantitative analyst, the market intuition of a seasoned portfolio manager, and the communication clarity of a top financial advisor.
|
|
87
|
-
|
|
88
|
-
CORE CAPABILITIES:
|
|
89
|
-
- Real-time market data analysis (quotes, charts, technicals) via yfinance and Polygon.io
|
|
90
|
-
- Fundamental analysis (financials, ratios, earnings, valuations)
|
|
91
|
-
- Technical analysis (RSI, MACD, Bollinger Bands, moving averages, support/resistance)
|
|
92
|
-
- Backtesting strategies (SMA crossover, RSI, MACD, Bollinger, momentum, breakout)
|
|
93
|
-
- Portfolio analysis and optimization
|
|
94
|
-
- Sector and market overview with economic indicators
|
|
95
|
-
- Insider trading and institutional activity tracking
|
|
96
|
-
- Financial news search and SEC filings analysis
|
|
97
|
-
|
|
98
|
-
RESPONSE PHILOSOPHY:
|
|
99
|
-
1. BE PROACTIVE: Anticipate follow-up questions and address them
|
|
100
|
-
2. BE THOROUGH: When analyzing, cover fundamentals + technicals + sentiment
|
|
101
|
-
3. BE ACTIONABLE: Always end with clear recommendations or next steps
|
|
102
|
-
4. BE DATA-DRIVEN: Cite specific numbers, dates, and percentages
|
|
103
|
-
5. BE HONEST: Acknowledge uncertainty and risks
|
|
104
|
-
|
|
105
|
-
RESPONSE FORMAT:
|
|
106
|
-
- Start with a 1-2 sentence executive summary (the key takeaway)
|
|
107
|
-
- Use structured sections with clear headers (## format)
|
|
108
|
-
- Present comparative data in markdown tables
|
|
109
|
-
- Highlight key metrics: **bold** for critical numbers
|
|
110
|
-
- Use bullet points for easy scanning
|
|
111
|
-
- End with "**Bottom Line:**" or "**Recommendation:**"
|
|
112
|
-
|
|
113
|
-
RATING SYSTEM (when asked for recommendations):
|
|
114
|
-
- STRONG BUY [A+]: Exceptional opportunity, high conviction
|
|
115
|
-
- BUY [A]: Favorable outlook, solid fundamentals
|
|
116
|
-
- HOLD [B]: Fair value, wait for better entry
|
|
117
|
-
- SELL [C]: Concerns outweigh positives
|
|
118
|
-
- STRONG SELL [D]: Significant risks, avoid
|
|
119
|
-
|
|
120
|
-
DATA GATHERING RULES:
|
|
121
|
-
- ALWAYS use tools to fetch current data before answering stock questions
|
|
122
|
-
- Use multiple tools for comprehensive analysis (quote + technicals + fundamentals)
|
|
123
|
-
- Cross-reference data points for accuracy
|
|
124
|
-
- If a tool fails, try alternative approaches or acknowledge the limitation
|
|
125
|
-
|
|
126
|
-
PROACTIVE INSIGHTS:
|
|
127
|
-
When analyzing any stock, proactively mention:
|
|
128
|
-
- Recent earnings surprises or upcoming earnings dates
|
|
129
|
-
- Major analyst rating changes
|
|
130
|
-
- Unusual volume or price movements
|
|
131
|
-
- Relevant sector trends
|
|
132
|
-
- Key support/resistance levels
|
|
133
|
-
- Comparison to peers when relevant
|
|
134
|
-
|
|
135
|
-
FORBIDDEN:
|
|
136
|
-
- Never fabricate data or prices
|
|
137
|
-
- Never provide data without fetching it first
|
|
138
|
-
- Never give buy/sell advice without disclosure of risks
|
|
139
|
-
- Never claim certainty about future performance
|
|
140
|
-
|
|
141
|
-
Remember: Users trust you for professional-grade financial research. Exceed their expectations with every response."""
|
|
142
|
-
|
|
143
|
-
# Enhanced autocomplete suggestions with more variety
|
|
144
44
|
SUGGESTIONS = [
|
|
145
|
-
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"analyze GOOGL",
|
|
149
|
-
"analyze NVDA",
|
|
150
|
-
"analyze TSLA",
|
|
151
|
-
"analyze META",
|
|
152
|
-
"analyze AMZN",
|
|
153
|
-
"analyze AMD",
|
|
154
|
-
"analyze SPY",
|
|
155
|
-
# Comparisons
|
|
156
|
-
"compare AAPL MSFT GOOGL",
|
|
157
|
-
"compare NVDA AMD INTC",
|
|
158
|
-
"compare META GOOGL AMZN",
|
|
159
|
-
"compare TSLA RIVN LCID",
|
|
160
|
-
# Technical
|
|
161
|
-
"technical analysis of AAPL",
|
|
162
|
-
"technical analysis of SPY",
|
|
163
|
-
"technical analysis of NVDA",
|
|
164
|
-
"technical analysis of QQQ",
|
|
165
|
-
# Backtesting
|
|
166
|
-
"backtest SMA crossover on AAPL",
|
|
167
|
-
"backtest RSI strategy on SPY",
|
|
168
|
-
"backtest MACD on NVDA",
|
|
169
|
-
"backtest momentum on QQQ",
|
|
170
|
-
# Market
|
|
171
|
-
"market overview",
|
|
172
|
-
"sector performance",
|
|
173
|
-
"what sectors are hot today",
|
|
174
|
-
# Quotes
|
|
175
|
-
"get quote for AAPL",
|
|
176
|
-
"price of NVDA",
|
|
177
|
-
"how is TSLA doing",
|
|
178
|
-
# Fundamentals
|
|
179
|
-
"fundamentals of MSFT",
|
|
180
|
-
"financials for AAPL",
|
|
181
|
-
"earnings of NVDA",
|
|
182
|
-
# Activity
|
|
183
|
-
"insider trading for AAPL",
|
|
184
|
-
"institutional holders of NVDA",
|
|
185
|
-
"analyst recommendations for TSLA",
|
|
186
|
-
# Natural language queries
|
|
187
|
-
"what should I know about AAPL",
|
|
188
|
-
"is NVDA overvalued",
|
|
189
|
-
"best tech stocks right now",
|
|
190
|
-
"should I buy TSLA",
|
|
191
|
-
# Commands
|
|
192
|
-
"/help",
|
|
193
|
-
"/clear",
|
|
194
|
-
"/keys",
|
|
195
|
-
"/models",
|
|
196
|
-
"/status",
|
|
197
|
-
"/backtest",
|
|
198
|
-
]
|
|
199
|
-
|
|
200
|
-
# Extended action verbs for smart completion
|
|
201
|
-
ACTION_VERBS = [
|
|
202
|
-
"analyze", "compare", "show", "get", "what is", "tell me about",
|
|
203
|
-
"technical analysis", "fundamentals", "price", "quote", "chart",
|
|
204
|
-
"backtest", "insider trading", "institutional", "analyst", "earnings",
|
|
205
|
-
"financials", "valuation", "news", "sector", "market", "portfolio"
|
|
45
|
+
"analyze", "backtest", "compare", "chart", "quote",
|
|
46
|
+
"AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "TSLA", "META",
|
|
47
|
+
"strategy", "momentum", "mean_reversion"
|
|
206
48
|
]
|
|
207
49
|
|
|
208
|
-
# Ticker categories for smart suggestions
|
|
209
|
-
TICKER_CATEGORIES = {
|
|
210
|
-
"tech": ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "AMD", "INTC", "CRM", "ADBE"],
|
|
211
|
-
"finance": ["JPM", "BAC", "GS", "MS", "V", "MA", "BRK.B", "C", "WFC", "AXP"],
|
|
212
|
-
"healthcare": ["JNJ", "UNH", "PFE", "MRK", "ABBV", "LLY", "TMO", "ABT", "BMY", "GILD"],
|
|
213
|
-
"consumer": ["AMZN", "TSLA", "HD", "NKE", "MCD", "SBUX", "COST", "WMT", "TGT", "LOW"],
|
|
214
|
-
"energy": ["XOM", "CVX", "COP", "SLB", "EOG", "MPC", "PSX", "VLO", "OXY", "HAL"],
|
|
215
|
-
"etf": ["SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK"],
|
|
216
|
-
"crypto": ["COIN", "MSTR", "RIOT", "MARA", "HUT"],
|
|
217
|
-
"ev": ["TSLA", "RIVN", "LCID", "NIO", "XPEV", "LI"],
|
|
218
|
-
"ai": ["NVDA", "AMD", "MSFT", "GOOGL", "META", "PLTR", "AI", "PATH", "SNOW"],
|
|
219
|
-
"semiconductor": ["NVDA", "AMD", "INTC", "AVGO", "QCOM", "TXN", "MU", "AMAT", "LRCX", "ASML"],
|
|
220
|
-
}
|
|
221
50
|
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
# SCREENS
|
|
53
|
+
# -----------------------------------------------------------------------------
|
|
222
54
|
|
|
223
|
-
|
|
224
|
-
"""
|
|
225
|
-
# Look for common patterns: $AAPL, or standalone uppercase words
|
|
226
|
-
# Only match if it's a known ticker or starts with $
|
|
227
|
-
words = text.upper().split()
|
|
228
|
-
tickers = []
|
|
55
|
+
class ThinkingStatus(Static):
|
|
56
|
+
"""Animated thinking indicator like Claude Code."""
|
|
229
57
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return list(dict.fromkeys(tickers)) # Dedupe while preserving order
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class SigmaSuggester(Suggester):
|
|
247
|
-
"""Smart autocomplete with fuzzy matching and context awareness."""
|
|
248
|
-
|
|
249
|
-
def __init__(self):
|
|
250
|
-
super().__init__(use_cache=False, case_sensitive=False)
|
|
251
|
-
self._build_suggestion_index()
|
|
252
|
-
|
|
253
|
-
def _build_suggestion_index(self):
|
|
254
|
-
"""Build an index of all possible suggestions for fast lookup."""
|
|
255
|
-
self.all_suggestions = []
|
|
256
|
-
|
|
257
|
-
# Add static suggestions
|
|
258
|
-
self.all_suggestions.extend(SUGGESTIONS)
|
|
259
|
-
|
|
260
|
-
# Generate dynamic suggestions for all tickers
|
|
261
|
-
for ticker in COMMON_TICKERS:
|
|
262
|
-
self.all_suggestions.extend([
|
|
263
|
-
f"analyze {ticker}",
|
|
264
|
-
f"technical analysis of {ticker}",
|
|
265
|
-
f"fundamentals of {ticker}",
|
|
266
|
-
f"price of {ticker}",
|
|
267
|
-
f"quote {ticker}",
|
|
268
|
-
f"insider trading {ticker}",
|
|
269
|
-
f"earnings {ticker}",
|
|
270
|
-
f"news {ticker}",
|
|
271
|
-
])
|
|
272
|
-
|
|
273
|
-
# Add category-based suggestions
|
|
274
|
-
for category, tickers in TICKER_CATEGORIES.items():
|
|
275
|
-
self.all_suggestions.append(f"best {category} stocks")
|
|
276
|
-
self.all_suggestions.append(f"{category} sector performance")
|
|
277
|
-
if len(tickers) >= 3:
|
|
278
|
-
self.all_suggestions.append(f"compare {' '.join(tickers[:3])}")
|
|
279
|
-
|
|
280
|
-
def _fuzzy_match(self, query: str, target: str) -> float:
|
|
281
|
-
"""Calculate fuzzy match score (0-1). Higher is better."""
|
|
282
|
-
query = query.lower()
|
|
283
|
-
target = target.lower()
|
|
284
|
-
|
|
285
|
-
# Exact prefix match is best
|
|
286
|
-
if target.startswith(query):
|
|
287
|
-
return 1.0 + len(query) / len(target)
|
|
288
|
-
|
|
289
|
-
# Check if all query chars appear in order
|
|
290
|
-
q_idx = 0
|
|
291
|
-
matches = 0
|
|
292
|
-
consecutive = 0
|
|
293
|
-
max_consecutive = 0
|
|
294
|
-
last_match = -1
|
|
295
|
-
|
|
296
|
-
for t_idx, char in enumerate(target):
|
|
297
|
-
if q_idx < len(query) and char == query[q_idx]:
|
|
298
|
-
matches += 1
|
|
299
|
-
if last_match == t_idx - 1:
|
|
300
|
-
consecutive += 1
|
|
301
|
-
max_consecutive = max(max_consecutive, consecutive)
|
|
302
|
-
else:
|
|
303
|
-
consecutive = 1
|
|
304
|
-
last_match = t_idx
|
|
305
|
-
q_idx += 1
|
|
306
|
-
|
|
307
|
-
if q_idx < len(query):
|
|
308
|
-
# Not all chars matched
|
|
309
|
-
# Try substring match
|
|
310
|
-
if query in target:
|
|
311
|
-
return 0.5
|
|
312
|
-
return 0
|
|
313
|
-
|
|
314
|
-
# Score based on match quality
|
|
315
|
-
score = (matches / len(query)) * 0.5 + (max_consecutive / len(query)) * 0.3
|
|
316
|
-
# Bonus for shorter targets (more relevant)
|
|
317
|
-
score += (1 - len(target) / 100) * 0.2
|
|
318
|
-
|
|
319
|
-
return score
|
|
320
|
-
|
|
321
|
-
def _get_context_suggestions(self, value: str) -> List[str]:
|
|
322
|
-
"""Get context-aware suggestions based on partial input."""
|
|
323
|
-
suggestions = []
|
|
324
|
-
value_lower = value.lower().strip()
|
|
325
|
-
|
|
326
|
-
# Command shortcuts
|
|
327
|
-
if value.startswith("/"):
|
|
328
|
-
cmd = value_lower[1:]
|
|
329
|
-
commands = ["/help", "/clear", "/keys", "/models", "/status", "/backtest",
|
|
330
|
-
"/provider", "/model", "/setkey", "/tickers"]
|
|
331
|
-
for c in commands:
|
|
332
|
-
if c[1:].startswith(cmd):
|
|
333
|
-
suggestions.append(c)
|
|
334
|
-
return suggestions
|
|
335
|
-
|
|
336
|
-
# Detect if user is typing a ticker
|
|
337
|
-
words = value.split()
|
|
338
|
-
last_word = words[-1] if words else ""
|
|
339
|
-
|
|
340
|
-
if last_word.startswith("$") or (last_word.isupper() and len(last_word) >= 1):
|
|
341
|
-
ticker_prefix = last_word.lstrip("$").upper()
|
|
342
|
-
matching_tickers = [t for t in COMMON_TICKERS if t.startswith(ticker_prefix)]
|
|
343
|
-
|
|
344
|
-
# If we have an action verb, complete with ticker
|
|
345
|
-
action_words = ["analyze", "compare", "technical", "price", "quote",
|
|
346
|
-
"fundamentals", "insider", "earnings", "news"]
|
|
347
|
-
prefix = " ".join(words[:-1]).lower() if len(words) > 1 else ""
|
|
348
|
-
|
|
349
|
-
for ticker in matching_tickers[:5]:
|
|
350
|
-
if prefix:
|
|
351
|
-
suggestions.append(f"{prefix} {ticker}")
|
|
352
|
-
else:
|
|
353
|
-
suggestions.append(f"analyze {ticker}")
|
|
354
|
-
return suggestions
|
|
355
|
-
|
|
356
|
-
# Natural language patterns
|
|
357
|
-
patterns = [
|
|
358
|
-
("ana", "analyze"),
|
|
359
|
-
("tech", "technical analysis of"),
|
|
360
|
-
("fun", "fundamentals of"),
|
|
361
|
-
("comp", "compare"),
|
|
362
|
-
("back", "backtest"),
|
|
363
|
-
("mark", "market overview"),
|
|
364
|
-
("sect", "sector performance"),
|
|
365
|
-
("pri", "price of"),
|
|
366
|
-
("quo", "quote"),
|
|
367
|
-
("ins", "insider trading"),
|
|
368
|
-
("ear", "earnings"),
|
|
369
|
-
("wha", "what should I know about"),
|
|
370
|
-
("sho", "should I buy"),
|
|
371
|
-
("is ", "is NVDA overvalued"),
|
|
372
|
-
("how", "how is"),
|
|
373
|
-
("bes", "best tech stocks"),
|
|
374
|
-
]
|
|
375
|
-
|
|
376
|
-
for prefix, expansion in patterns:
|
|
377
|
-
if value_lower.startswith(prefix):
|
|
378
|
-
if expansion.endswith("of") or expansion.endswith("about"):
|
|
379
|
-
# Add popular tickers
|
|
380
|
-
for ticker in ["AAPL", "NVDA", "MSFT", "TSLA", "GOOGL"]:
|
|
381
|
-
suggestions.append(f"{expansion} {ticker}")
|
|
382
|
-
else:
|
|
383
|
-
suggestions.append(expansion)
|
|
384
|
-
|
|
385
|
-
return suggestions
|
|
58
|
+
DEFAULT_CSS = """
|
|
59
|
+
ThinkingStatus {
|
|
60
|
+
height: 1;
|
|
61
|
+
width: 100%;
|
|
62
|
+
color: #a1a1aa;
|
|
63
|
+
display: none;
|
|
64
|
+
padding-left: 2;
|
|
65
|
+
}
|
|
66
|
+
ThinkingStatus.visible {
|
|
67
|
+
display: block;
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
386
70
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
# Sort by score and return best
|
|
407
|
-
scored.sort(key=lambda x: x[0], reverse=True)
|
|
408
|
-
|
|
409
|
-
if scored:
|
|
410
|
-
return scored[0][1]
|
|
411
|
-
|
|
412
|
-
# Last resort: if looks like a ticker, suggest analyze
|
|
413
|
-
if value.isupper() and len(value) <= 5 and value.isalpha():
|
|
414
|
-
return f"analyze {value}"
|
|
71
|
+
def on_mount(self):
|
|
72
|
+
self.loading = False
|
|
73
|
+
self.frame = 0
|
|
74
|
+
self.frames = ["σ", "o", "O", "0", "O", "o"]
|
|
75
|
+
self.set_interval(0.1, self.animate)
|
|
76
|
+
|
|
77
|
+
def animate(self):
|
|
78
|
+
if self.loading:
|
|
79
|
+
self.frame = (self.frame + 1) % len(self.frames)
|
|
80
|
+
# symbol = self.frames[self.frame]
|
|
81
|
+
# Just pulse the sigma
|
|
82
|
+
if self.frame < 3:
|
|
83
|
+
self.update(Text("σ", style="bold #d97757") + " Analysis in progress...")
|
|
84
|
+
else:
|
|
85
|
+
self.update(Text("σ", style="dim #d97757") + " Analysis in progress...")
|
|
86
|
+
|
|
87
|
+
def start(self):
|
|
88
|
+
self.loading = True
|
|
89
|
+
self.add_class("visible")
|
|
415
90
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
CSS = """
|
|
420
|
-
Screen {
|
|
421
|
-
background: #0a0a0f;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
* {
|
|
425
|
-
scrollbar-size: 1 1;
|
|
426
|
-
scrollbar-color: #3b82f6 30%;
|
|
427
|
-
scrollbar-color-hover: #60a5fa 50%;
|
|
428
|
-
scrollbar-color-active: #93c5fd 70%;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
#main-container {
|
|
432
|
-
width: 100%;
|
|
433
|
-
height: 100%;
|
|
434
|
-
background: #0a0a0f;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
#chat-area {
|
|
438
|
-
height: 1fr;
|
|
439
|
-
margin: 1 2;
|
|
440
|
-
background: #0a0a0f;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
#chat-log {
|
|
444
|
-
background: #0a0a0f;
|
|
445
|
-
padding: 1 0;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
#status-bar {
|
|
449
|
-
height: 3;
|
|
450
|
-
background: #0f1419;
|
|
451
|
-
border-top: solid #1e293b;
|
|
452
|
-
padding: 0 2;
|
|
453
|
-
dock: bottom;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
#status-content {
|
|
457
|
-
width: 100%;
|
|
458
|
-
height: 100%;
|
|
459
|
-
content-align: left middle;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
#thinking-indicator {
|
|
463
|
-
width: auto;
|
|
464
|
-
height: 1;
|
|
465
|
-
content-align: center middle;
|
|
466
|
-
display: none;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
#thinking-indicator.visible {
|
|
470
|
-
display: block;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
#tool-calls-display {
|
|
474
|
-
width: 100%;
|
|
475
|
-
height: auto;
|
|
476
|
-
max-height: 8;
|
|
477
|
-
background: #0f1419;
|
|
478
|
-
border: round #1e293b;
|
|
479
|
-
margin: 0 2;
|
|
480
|
-
padding: 0 1;
|
|
481
|
-
display: none;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
#tool-calls-display.visible {
|
|
485
|
-
display: block;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
#input-area {
|
|
489
|
-
height: 5;
|
|
490
|
-
padding: 1 2;
|
|
491
|
-
background: #0f1419;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
#input-row {
|
|
495
|
-
height: 3;
|
|
496
|
-
width: 100%;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
#sigma-indicator {
|
|
500
|
-
width: 4;
|
|
501
|
-
height: 3;
|
|
502
|
-
content-align: center middle;
|
|
503
|
-
background: transparent;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
#prompt-input {
|
|
507
|
-
width: 1fr;
|
|
508
|
-
background: #1e293b;
|
|
509
|
-
border: tall #3b82f6;
|
|
510
|
-
color: #f8fafc;
|
|
511
|
-
padding: 0 1;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
#prompt-input:focus {
|
|
515
|
-
border: tall #60a5fa;
|
|
516
|
-
background: #1e3a5f;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
#prompt-input.-autocomplete {
|
|
520
|
-
border: tall #22c55e;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
#ticker-highlight {
|
|
524
|
-
width: auto;
|
|
525
|
-
height: 1;
|
|
526
|
-
padding: 0 1;
|
|
527
|
-
background: transparent;
|
|
528
|
-
color: #22d3ee;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
Footer {
|
|
532
|
-
background: #0d1117;
|
|
533
|
-
height: 1;
|
|
534
|
-
dock: bottom;
|
|
535
|
-
}
|
|
91
|
+
def stop(self):
|
|
92
|
+
self.loading = False
|
|
93
|
+
self.remove_class("visible")
|
|
536
94
|
|
|
537
|
-
Footer > .footer--highlight {
|
|
538
|
-
background: transparent;
|
|
539
|
-
}
|
|
540
95
|
|
|
541
|
-
|
|
542
|
-
background: #1a1a2e;
|
|
543
|
-
color: #f59e0b;
|
|
544
|
-
text-style: bold;
|
|
545
|
-
}
|
|
96
|
+
class SplashScreen(Screen):
|
|
546
97
|
|
|
547
|
-
|
|
548
|
-
color: #6b7280;
|
|
549
|
-
}
|
|
98
|
+
BINDINGS = [("enter", "start_app", "Start")]
|
|
550
99
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
display: none;
|
|
559
|
-
}
|
|
100
|
+
def compose(self) -> ComposeResult:
|
|
101
|
+
yield Container(
|
|
102
|
+
Static(Panel(Align.center("[#e4e4e7]Welcome to Sigma[/]"), border_style="#d97757", padding=(0, 2)), id="welcome-badge"),
|
|
103
|
+
Static(Align.center(SIGMA_ASCII), id="ascii-art"),
|
|
104
|
+
Label("Press Enter to continue", id="press-enter"),
|
|
105
|
+
id="splash-container"
|
|
106
|
+
)
|
|
560
107
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
"""
|
|
108
|
+
def action_start_app(self):
|
|
109
|
+
self.app.switch_screen("main")
|
|
565
110
|
|
|
566
111
|
|
|
567
|
-
class
|
|
568
|
-
"""
|
|
569
|
-
|
|
570
|
-
def __init__(self, *args, **kwargs):
|
|
571
|
-
super().__init__(*args, **kwargs)
|
|
572
|
-
self.tool_calls: List[dict] = []
|
|
573
|
-
self.frame = 0
|
|
574
|
-
self.timer = None
|
|
575
|
-
|
|
576
|
-
def add_tool_call(self, name: str, status: str = "running"):
|
|
577
|
-
"""Add a tool call to the display."""
|
|
578
|
-
self.tool_calls.append({"name": name, "status": status, "frame": 0})
|
|
579
|
-
self.add_class("visible")
|
|
580
|
-
self._render()
|
|
581
|
-
if not self.timer:
|
|
582
|
-
self.timer = self.set_interval(0.1, self._animate)
|
|
112
|
+
class MainScreen(Screen):
|
|
113
|
+
"""Main chat interface."""
|
|
583
114
|
|
|
584
|
-
|
|
585
|
-
"""
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
tc["status"] = "complete"
|
|
589
|
-
break
|
|
590
|
-
self._render()
|
|
591
|
-
|
|
592
|
-
def clear(self):
|
|
593
|
-
"""Clear all tool calls."""
|
|
594
|
-
self.tool_calls = []
|
|
595
|
-
if self.timer:
|
|
596
|
-
self.timer.stop()
|
|
597
|
-
self.timer = None
|
|
598
|
-
self.remove_class("visible")
|
|
599
|
-
self.update("")
|
|
600
|
-
|
|
601
|
-
def _animate(self):
|
|
602
|
-
"""Animate the spinner."""
|
|
603
|
-
self.frame = (self.frame + 1) % len(TOOL_SPINNER_FRAMES)
|
|
604
|
-
for tc in self.tool_calls:
|
|
605
|
-
if tc["status"] == "running":
|
|
606
|
-
tc["frame"] = self.frame
|
|
607
|
-
self._render()
|
|
608
|
-
|
|
609
|
-
def _render(self):
|
|
610
|
-
"""Render the tool calls display."""
|
|
611
|
-
if not self.tool_calls:
|
|
612
|
-
return
|
|
613
|
-
|
|
614
|
-
lines = []
|
|
615
|
-
for tc in self.tool_calls:
|
|
616
|
-
if tc["status"] == "running":
|
|
617
|
-
spinner = TOOL_SPINNER_FRAMES[tc["frame"] % len(TOOL_SPINNER_FRAMES)]
|
|
618
|
-
lines.append(f" [cyan]{spinner}[/cyan] [bold]{tc['name']}[/bold] [dim]executing...[/dim]")
|
|
619
|
-
else:
|
|
620
|
-
lines.append(f" [green][ok][/green] [bold]{tc['name']}[/bold] [green]done[/green]")
|
|
621
|
-
|
|
622
|
-
self.update(Text.from_markup("\n".join(lines)))
|
|
115
|
+
BINDINGS = [
|
|
116
|
+
Binding("ctrl+l", "clear_chat", "Clear"),
|
|
117
|
+
Binding("ctrl+b", "toggle_sidebar", "Sidebar"),
|
|
118
|
+
]
|
|
623
119
|
|
|
120
|
+
def compose(self) -> ComposeResult:
|
|
121
|
+
with Horizontal():
|
|
122
|
+
# Main Chat Area
|
|
123
|
+
with Vertical(id="chat-area"):
|
|
124
|
+
# Use a specific class or check if can_focus can be disabled here for RichLog
|
|
125
|
+
# In Textual, RichLog is focusable by default. We disable it to prevent it stealing focus from Input.
|
|
126
|
+
log = RichLog(id="chat-log", wrap=True, highlight=True, markup=True)
|
|
127
|
+
log.can_focus = False
|
|
128
|
+
yield log
|
|
129
|
+
yield ThinkingStatus(id="thinking")
|
|
130
|
+
yield Input(
|
|
131
|
+
placeholder="Ask Sigma...",
|
|
132
|
+
id="prompt",
|
|
133
|
+
suggester=SuggestFromList(SUGGESTIONS, case_sensitive=False)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Sidebar (Hidden by default)
|
|
137
|
+
with Vertical(id="sidebar"):
|
|
138
|
+
yield Label("TRACE", classes="sidebar-header")
|
|
139
|
+
tracelog = RichLog(id="trace-log", wrap=True, highlight=True, markup=True)
|
|
140
|
+
tracelog.can_focus = False
|
|
141
|
+
yield tracelog
|
|
624
142
|
|
|
625
|
-
class SigmaIndicator(Static):
|
|
626
|
-
"""Animated sigma indicator - typewriter style when active."""
|
|
627
|
-
|
|
628
|
-
def __init__(self, *args, **kwargs):
|
|
629
|
-
super().__init__(*args, **kwargs)
|
|
630
|
-
self.active = False
|
|
631
|
-
self.mode = "idle" # idle, thinking, tool
|
|
632
|
-
self.frame = 0
|
|
633
|
-
self.timer = None
|
|
634
|
-
|
|
635
143
|
def on_mount(self):
|
|
636
|
-
self.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
interval = 0.08 if mode == "thinking" else 0.12
|
|
644
|
-
self.timer = self.set_interval(interval, self._animate)
|
|
645
|
-
elif not active and self.timer:
|
|
646
|
-
self.timer.stop()
|
|
647
|
-
self.timer = None
|
|
648
|
-
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
649
|
-
|
|
650
|
-
def _animate(self):
|
|
651
|
-
if self.mode == "thinking":
|
|
652
|
-
# Typewriter effect: s -> si -> sig -> sigm -> sigma -> sigm -> ...
|
|
653
|
-
self.frame = (self.frame + 1) % len(SIGMA_FRAMES)
|
|
654
|
-
self.update(Text.from_markup(SIGMA_FRAMES[self.frame]))
|
|
655
|
-
else:
|
|
656
|
-
# Pulse effect for tool calls
|
|
657
|
-
self.frame = (self.frame + 1) % len(SIGMA_PULSE_FRAMES)
|
|
658
|
-
self.update(Text.from_markup(SIGMA_PULSE_FRAMES[self.frame]))
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
class TickerHighlight(Static):
|
|
662
|
-
"""Display detected tickers in real-time."""
|
|
663
|
-
|
|
664
|
-
def update_tickers(self, text: str):
|
|
665
|
-
"""Update displayed tickers based on input."""
|
|
666
|
-
tickers = extract_tickers(text)
|
|
667
|
-
if tickers:
|
|
668
|
-
ticker_text = " ".join([f"[cyan]${t}[/cyan]" for t in tickers[:3]])
|
|
669
|
-
self.update(Text.from_markup(ticker_text))
|
|
670
|
-
else:
|
|
671
|
-
self.update("")
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
class ChatLog(RichLog):
|
|
675
|
-
"""Chat log with rich formatting."""
|
|
676
|
-
|
|
677
|
-
def write_user(self, message: str):
|
|
678
|
-
# Highlight any tickers in user message
|
|
679
|
-
highlighted = message
|
|
680
|
-
for ticker in extract_tickers(message):
|
|
681
|
-
highlighted = re.sub(
|
|
682
|
-
rf'\b{ticker}\b',
|
|
683
|
-
f'[cyan]{ticker}[/cyan]',
|
|
684
|
-
highlighted,
|
|
685
|
-
flags=re.IGNORECASE
|
|
144
|
+
self.query_one("#prompt").focus()
|
|
145
|
+
# Initial greeting
|
|
146
|
+
self.query_one("#chat-log", RichLog).write(
|
|
147
|
+
Text.assemble(
|
|
148
|
+
("σ ", "bold #d97757"),
|
|
149
|
+
("Sigma initialized. ", "bold #e4e4e7"),
|
|
150
|
+
("Ready for research.", "dim #a1a1aa")
|
|
686
151
|
)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
Text.from_markup(highlighted) if '[cyan]' in highlighted else Text(message, style="white"),
|
|
690
|
-
title="[bold blue]You[/bold blue]",
|
|
691
|
-
border_style="blue",
|
|
692
|
-
padding=(0, 1),
|
|
693
|
-
))
|
|
694
|
-
|
|
695
|
-
def write_assistant(self, message: str):
|
|
696
|
-
self.write(Panel(
|
|
697
|
-
Markdown(message),
|
|
698
|
-
title=f"[bold cyan]{SIGMA} Sigma[/bold cyan]",
|
|
699
|
-
border_style="cyan",
|
|
700
|
-
padding=(0, 1),
|
|
701
|
-
))
|
|
702
|
-
|
|
703
|
-
def write_tool(self, tool_name: str):
|
|
704
|
-
# This is now handled by ToolCallDisplay
|
|
705
|
-
pass
|
|
706
|
-
|
|
707
|
-
def write_error(self, message: str):
|
|
708
|
-
self.write(Panel(Text(message, style="red"), title="[red]Error[/red]", border_style="red"))
|
|
709
|
-
|
|
710
|
-
def write_system(self, message: str):
|
|
711
|
-
self.write(Text.from_markup(f"[dim]{message}[/dim]"))
|
|
712
|
-
|
|
713
|
-
def write_welcome(self):
|
|
714
|
-
self.write(Text.from_markup(WELCOME_BANNER))
|
|
152
|
+
)
|
|
153
|
+
|
|
715
154
|
|
|
155
|
+
# -----------------------------------------------------------------------------
|
|
156
|
+
# MAIN APPLICATION
|
|
157
|
+
# -----------------------------------------------------------------------------
|
|
716
158
|
|
|
717
159
|
class SigmaApp(App):
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
160
|
+
CSS = """
|
|
161
|
+
/* --- COLOR PALETTE (Claude Code Dark) --- */
|
|
162
|
+
$bg: #0f0f0f; /* Very dark grey/black */
|
|
163
|
+
$surface: #1a1a1a; /* Slightly lighter surface */
|
|
164
|
+
$text: #e4e4e7; /* Off-white */
|
|
165
|
+
$dim: #a1a1aa; /* Muted text */
|
|
166
|
+
$accent: #d97757; /* Claude-like Orange/Peach */
|
|
167
|
+
$blue: #3b82f6; /* Link/Action Blue */
|
|
168
|
+
|
|
169
|
+
Screen {
|
|
170
|
+
background: $bg;
|
|
171
|
+
color: $text;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* --- SPLASH SCREEN --- */
|
|
175
|
+
#splash-container {
|
|
176
|
+
align: center middle;
|
|
177
|
+
height: 100%;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#welcome-badge {
|
|
181
|
+
width: auto;
|
|
182
|
+
margin-bottom: 2;
|
|
183
|
+
background: $bg;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#ascii-art {
|
|
187
|
+
margin-bottom: 4;
|
|
188
|
+
color: $accent;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#press-enter {
|
|
192
|
+
color: $dim;
|
|
193
|
+
text-style: bold;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* --- MAIN CHAT LAYOUT --- */
|
|
197
|
+
#chat-area {
|
|
198
|
+
width: 1fr;
|
|
199
|
+
height: 100%;
|
|
200
|
+
padding: 1 2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#chat-log {
|
|
204
|
+
width: 100%;
|
|
205
|
+
height: 1fr;
|
|
206
|
+
background: $bg;
|
|
207
|
+
border: none;
|
|
208
|
+
margin-bottom: 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Input {
|
|
212
|
+
width: 100%;
|
|
213
|
+
height: 3;
|
|
214
|
+
background: $surface;
|
|
215
|
+
border: solid $dim;
|
|
216
|
+
color: $text;
|
|
217
|
+
padding: 0 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Input:focus {
|
|
221
|
+
border: solid $accent;
|
|
222
|
+
background: $surface;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Input .suggestion {
|
|
226
|
+
color: $dim;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* --- SIDEBAR --- */
|
|
230
|
+
#sidebar {
|
|
231
|
+
width: 40;
|
|
232
|
+
dock: right;
|
|
233
|
+
background: $surface;
|
|
234
|
+
border-left: solid $dim 20%;
|
|
235
|
+
display: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#sidebar.visible {
|
|
239
|
+
display: block;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.sidebar-header {
|
|
243
|
+
background: $surface;
|
|
244
|
+
color: $dim;
|
|
245
|
+
text-align: center;
|
|
246
|
+
text-style: bold;
|
|
247
|
+
padding: 1;
|
|
248
|
+
border-bottom: solid $dim 20%;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#trace-log {
|
|
252
|
+
background: $surface;
|
|
253
|
+
padding: 1;
|
|
254
|
+
}
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
SCREENS = {
|
|
258
|
+
"splash": SplashScreen,
|
|
259
|
+
"main": MainScreen
|
|
260
|
+
}
|
|
261
|
+
|
|
766
262
|
def on_mount(self):
|
|
767
|
-
|
|
768
|
-
|
|
263
|
+
# Initialize Core Logic
|
|
264
|
+
self.router = CommandRouter()
|
|
265
|
+
try:
|
|
266
|
+
self.llm_router = get_router(get_settings())
|
|
267
|
+
except Exception:
|
|
268
|
+
self.llm_router = None
|
|
269
|
+
|
|
270
|
+
self.push_screen("splash")
|
|
271
|
+
|
|
272
|
+
@on(Input.Submitted)
|
|
273
|
+
async def on_input(self, event: Input.Submitted):
|
|
274
|
+
query = event.value
|
|
275
|
+
if not query.strip(): return
|
|
769
276
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
chat.write_system(f"{SIGMA} Type [cyan]/help[/cyan] for commands • [cyan]/keys[/cyan] to set up API keys")
|
|
773
|
-
chat.write_system("")
|
|
277
|
+
# Clear input
|
|
278
|
+
event.input.value = ""
|
|
774
279
|
|
|
775
|
-
|
|
776
|
-
self.query_one
|
|
777
|
-
|
|
778
|
-
def _init_llm(self):
|
|
779
|
-
"""Initialize the LLM client with proper error handling."""
|
|
280
|
+
# Get chat log - relative to the screen containing the input
|
|
281
|
+
# This fixes the issue where self.query_one fails on App or wrong screen
|
|
780
282
|
try:
|
|
781
|
-
|
|
782
|
-
except SigmaError as e:
|
|
783
|
-
chat = self.query_one("#chat-log", ChatLog)
|
|
784
|
-
chat.write_error(f"[E{e.code}] {e.message}")
|
|
785
|
-
if e.details.get("hint"):
|
|
786
|
-
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
787
|
-
self.llm = None
|
|
283
|
+
chat_log = event.control.screen.query_one("#chat-log", RichLog)
|
|
788
284
|
except Exception as e:
|
|
789
|
-
|
|
790
|
-
chat.write_error(f"[E{ErrorCode.PROVIDER_ERROR}] Failed to initialize: {str(e)[:100]}")
|
|
791
|
-
chat.write_system("[dim]Use /keys to configure API keys[/dim]")
|
|
792
|
-
self.llm = None
|
|
793
|
-
|
|
794
|
-
@on(Input.Changed)
|
|
795
|
-
def on_input_change(self, event: Input.Changed):
|
|
796
|
-
"""Update ticker highlight as user types."""
|
|
797
|
-
ticker_display = self.query_one("#ticker-highlight", TickerHighlight)
|
|
798
|
-
ticker_display.update_tickers(event.value)
|
|
799
|
-
|
|
800
|
-
@on(Input.Submitted)
|
|
801
|
-
def handle_input(self, event: Input.Submitted):
|
|
802
|
-
if self.is_processing:
|
|
285
|
+
self.notify(f"UI Error: Could not find chat log on active screen. {e}", severity="error")
|
|
803
286
|
return
|
|
804
|
-
|
|
805
|
-
text = event.value.strip()
|
|
806
|
-
if not text:
|
|
807
|
-
return
|
|
808
|
-
|
|
809
|
-
self.query_one("#prompt-input", Input).value = ""
|
|
810
|
-
self.history.append(text)
|
|
811
|
-
self.history_idx = len(self.history)
|
|
812
|
-
|
|
813
|
-
chat = self.query_one("#chat-log", ChatLog)
|
|
814
|
-
|
|
815
|
-
if text.startswith("/"):
|
|
816
|
-
self._handle_command(text, chat)
|
|
817
|
-
else:
|
|
818
|
-
chat.write_user(text)
|
|
819
|
-
self._process_query(text, chat)
|
|
820
|
-
|
|
821
|
-
def _handle_command(self, cmd: str, chat: ChatLog):
|
|
822
|
-
parts = cmd.lower().split()
|
|
823
|
-
command = parts[0]
|
|
824
|
-
args = parts[1:] if len(parts) > 1 else []
|
|
825
|
-
|
|
826
|
-
if command == "/help":
|
|
827
|
-
self._show_comprehensive_help(chat)
|
|
828
|
-
elif command == "/clear":
|
|
829
|
-
chat.clear()
|
|
830
|
-
self.conversation = []
|
|
831
|
-
chat.write_system("Chat cleared")
|
|
832
|
-
elif command == "/keys":
|
|
833
|
-
self._show_keys(chat)
|
|
834
|
-
elif command == "/models":
|
|
835
|
-
self._show_models(chat)
|
|
836
|
-
elif command == "/status":
|
|
837
|
-
self._show_status(chat)
|
|
838
|
-
elif command == "/backtest":
|
|
839
|
-
self._show_strategies(chat)
|
|
840
|
-
elif command == "/provider" and args:
|
|
841
|
-
self._switch_provider(args[0], chat)
|
|
842
|
-
elif command == "/model" and args:
|
|
843
|
-
self._switch_model(args[0], chat)
|
|
844
|
-
elif command.startswith("/setkey") and len(parts) >= 3:
|
|
845
|
-
self._set_key(parts[1], parts[2], chat)
|
|
846
|
-
elif command == "/tickers":
|
|
847
|
-
self._show_popular_tickers(chat)
|
|
848
|
-
else:
|
|
849
|
-
chat.write_error(f"Unknown command: {command}. Type /help for available commands.")
|
|
850
|
-
|
|
851
|
-
def _show_comprehensive_help(self, chat: ChatLog):
|
|
852
|
-
"""Show comprehensive help with examples."""
|
|
853
|
-
help_text = f"""
|
|
854
|
-
[bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
|
|
855
|
-
[bold] {SIGMA} SIGMA HELP CENTER [/bold]
|
|
856
|
-
[bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
|
|
857
|
-
|
|
858
|
-
[bold yellow]QUICK START[/bold yellow]
|
|
859
|
-
Just type naturally! Examples:
|
|
860
|
-
• "analyze AAPL" - Full analysis of Apple
|
|
861
|
-
• "compare NVDA AMD INTC" - Compare multiple stocks
|
|
862
|
-
• "is TSLA overvalued?" - Get AI insights
|
|
863
|
-
• "market overview" - See major indices
|
|
864
|
-
|
|
865
|
-
[bold yellow]COMMANDS[/bold yellow]
|
|
866
|
-
[cyan]/help[/cyan] This help screen
|
|
867
|
-
[cyan]/clear[/cyan] Clear chat history
|
|
868
|
-
[cyan]/keys[/cyan] Configure API keys
|
|
869
|
-
[cyan]/models[/cyan] Show available models
|
|
870
|
-
[cyan]/status[/cyan] Current configuration
|
|
871
|
-
[cyan]/backtest[/cyan] Show backtest strategies
|
|
872
|
-
[cyan]/provider <name>[/cyan] Switch AI provider
|
|
873
|
-
[cyan]/model <name>[/cyan] Switch model
|
|
874
|
-
[cyan]/setkey <p> <k>[/cyan] Set API key
|
|
875
|
-
[cyan]/tickers[/cyan] Popular tickers list
|
|
876
|
-
|
|
877
|
-
[bold yellow]ANALYSIS EXAMPLES[/bold yellow]
|
|
878
|
-
• "technical analysis of SPY"
|
|
879
|
-
• "fundamentals of MSFT"
|
|
880
|
-
• "insider trading for AAPL"
|
|
881
|
-
• "analyst recommendations for NVDA"
|
|
882
|
-
• "sector performance"
|
|
883
|
-
|
|
884
|
-
[bold yellow]BACKTESTING[/bold yellow]
|
|
885
|
-
• "backtest SMA crossover on AAPL"
|
|
886
|
-
• "backtest RSI strategy on SPY"
|
|
887
|
-
• "backtest MACD on NVDA"
|
|
888
|
-
Strategies: sma_crossover, rsi, macd, bollinger, momentum, breakout
|
|
889
|
-
|
|
890
|
-
[bold yellow]KEYBOARD SHORTCUTS[/bold yellow]
|
|
891
|
-
[bold]Tab[/bold] Autocomplete suggestion
|
|
892
|
-
[bold]Ctrl+L[/bold] Clear chat
|
|
893
|
-
[bold]Ctrl+M[/bold] Show models
|
|
894
|
-
[bold]Ctrl+H[/bold] Toggle quick help
|
|
895
|
-
[bold]Ctrl+P[/bold] Command palette
|
|
896
|
-
[bold]Esc[/bold] Cancel operation
|
|
897
|
-
|
|
898
|
-
[bold yellow]TIPS[/bold yellow]
|
|
899
|
-
• Type [cyan]$AAPL[/cyan] or [cyan]AAPL[/cyan] - tickers auto-detected
|
|
900
|
-
• Use Tab for smart autocomplete
|
|
901
|
-
• Detected tickers shown next to input
|
|
902
|
-
"""
|
|
903
|
-
chat.write(Panel(
|
|
904
|
-
Text.from_markup(help_text),
|
|
905
|
-
title=f"[bold cyan]{SIGMA} Help[/bold cyan]",
|
|
906
|
-
border_style="cyan",
|
|
907
|
-
padding=(0, 1),
|
|
908
|
-
))
|
|
909
|
-
|
|
910
|
-
def _show_popular_tickers(self, chat: ChatLog):
|
|
911
|
-
"""Show popular tickers organized by category."""
|
|
912
|
-
tickers_text = """
|
|
913
|
-
[bold]Tech Giants[/bold]: AAPL, MSFT, GOOGL, AMZN, META, NVDA
|
|
914
|
-
[bold]Semiconductors[/bold]: NVDA, AMD, INTC, AVGO, QCOM, TSM
|
|
915
|
-
[bold]EVs & Auto[/bold]: TSLA, RIVN, LCID, F, GM
|
|
916
|
-
[bold]Finance[/bold]: JPM, BAC, GS, MS, V, MA
|
|
917
|
-
[bold]Healthcare[/bold]: JNJ, PFE, UNH, MRK, ABBV
|
|
918
|
-
[bold]ETFs[/bold]: SPY, QQQ, IWM, DIA, VTI, VOO
|
|
919
|
-
[bold]Sector ETFs[/bold]: XLK, XLF, XLE, XLV, XLI
|
|
920
|
-
"""
|
|
921
|
-
chat.write(Panel(
|
|
922
|
-
Text.from_markup(tickers_text),
|
|
923
|
-
title=f"[cyan]{SIGMA} Popular Tickers[/cyan]",
|
|
924
|
-
border_style="dim",
|
|
925
|
-
))
|
|
926
|
-
|
|
927
|
-
def _show_keys(self, chat: ChatLog):
|
|
928
|
-
"""Show comprehensive API key management interface."""
|
|
929
|
-
keys_help = f"""
|
|
930
|
-
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
931
|
-
[bold] {SIGMA} API KEY MANAGER [/bold]
|
|
932
|
-
[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]
|
|
933
|
-
|
|
934
|
-
[bold yellow]QUICK SETUP[/bold yellow]
|
|
935
|
-
Set a key: [cyan]/setkey <provider> <your-api-key>[/cyan]
|
|
936
|
-
|
|
937
|
-
[bold yellow]LLM PROVIDERS[/bold yellow]
|
|
938
|
-
[bold]google[/bold] → https://aistudio.google.com/apikey
|
|
939
|
-
[bold]openai[/bold] → https://platform.openai.com/api-keys
|
|
940
|
-
[bold]anthropic[/bold] → https://console.anthropic.com/settings/keys
|
|
941
|
-
[bold]groq[/bold] → https://console.groq.com/keys [dim](free!)[/dim]
|
|
942
|
-
[bold]xai[/bold] → https://console.x.ai
|
|
943
|
-
|
|
944
|
-
[bold yellow]DATA PROVIDERS[/bold yellow] [dim](optional - enhances data quality)[/dim]
|
|
945
|
-
[bold]polygon[/bold] → https://polygon.io/dashboard/api-keys
|
|
946
287
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
/setkey openai sk-proj-...
|
|
950
|
-
/setkey polygon abc123...
|
|
951
|
-
/provider groq [dim]← switch to groq[/dim]
|
|
952
|
-
|
|
953
|
-
[bold yellow]TIPS[/bold yellow]
|
|
954
|
-
• [green]Groq[/green] is free and fast - great for starting out
|
|
955
|
-
• [green]Ollama[/green] runs locally - no key needed (/provider ollama)
|
|
956
|
-
• Keys are stored in [dim]~/.sigma/config.env[/dim]
|
|
957
|
-
"""
|
|
958
|
-
chat.write(Panel(
|
|
959
|
-
Text.from_markup(keys_help),
|
|
960
|
-
title=f"[bold cyan]{SIGMA} API Keys[/bold cyan]",
|
|
961
|
-
border_style="cyan",
|
|
962
|
-
padding=(0, 1),
|
|
963
|
-
))
|
|
964
|
-
self._show_status(chat)
|
|
965
|
-
|
|
966
|
-
def _show_status(self, chat: ChatLog):
|
|
967
|
-
"""Show current configuration status."""
|
|
968
|
-
table = Table(show_header=True, box=None, padding=(0, 2), title=f"{SIGMA} Current Status")
|
|
969
|
-
table.add_column("Setting", style="bold")
|
|
970
|
-
table.add_column("Value")
|
|
971
|
-
table.add_column("Status")
|
|
972
|
-
|
|
973
|
-
provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
|
|
974
|
-
table.add_row("Provider", provider, "[green][*][/green] Active")
|
|
975
|
-
table.add_row("Model", self.settings.default_model, "")
|
|
976
|
-
table.add_row("", "", "")
|
|
977
|
-
|
|
978
|
-
# LLM Keys
|
|
979
|
-
llm_keys = [
|
|
980
|
-
("Google", self.settings.google_api_key, "google"),
|
|
981
|
-
("OpenAI", self.settings.openai_api_key, "openai"),
|
|
982
|
-
("Anthropic", self.settings.anthropic_api_key, "anthropic"),
|
|
983
|
-
("Groq", self.settings.groq_api_key, "groq"),
|
|
984
|
-
("xAI", self.settings.xai_api_key, "xai"),
|
|
985
|
-
]
|
|
986
|
-
for name, key, prov in llm_keys:
|
|
987
|
-
if key:
|
|
988
|
-
masked = key[:8] + "..." + key[-4:] if len(key) > 12 else "***"
|
|
989
|
-
status = "[green][ok][/green]"
|
|
990
|
-
else:
|
|
991
|
-
masked = "[dim]not set[/dim]"
|
|
992
|
-
status = "[dim]--[/dim]"
|
|
993
|
-
|
|
994
|
-
# Highlight active provider
|
|
995
|
-
if prov == provider:
|
|
996
|
-
name = f"[bold cyan]{name}[/bold cyan]"
|
|
997
|
-
table.add_row(f" {name}", Text.from_markup(masked), Text.from_markup(status))
|
|
998
|
-
|
|
999
|
-
table.add_row("", "", "")
|
|
1000
|
-
|
|
1001
|
-
# Data Keys
|
|
1002
|
-
polygon_key = getattr(self.settings, 'polygon_api_key', None)
|
|
1003
|
-
if polygon_key:
|
|
1004
|
-
table.add_row(" Polygon", polygon_key[:8] + "...", Text.from_markup("[green][ok][/green]"))
|
|
1005
|
-
else:
|
|
1006
|
-
table.add_row(" Polygon", Text.from_markup("[dim]not set[/dim]"), Text.from_markup("[dim]optional[/dim]"))
|
|
288
|
+
# Prepare display text
|
|
289
|
+
display_query = escape(query)
|
|
1007
290
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
def _show_models(self, chat: ChatLog):
|
|
1011
|
-
table = Table(title=f"{SIGMA} Models", show_header=True, border_style="dim")
|
|
1012
|
-
table.add_column("Provider", style="cyan")
|
|
1013
|
-
table.add_column("Models")
|
|
1014
|
-
for p, m in AVAILABLE_MODELS.items():
|
|
1015
|
-
table.add_row(p, ", ".join(m))
|
|
1016
|
-
chat.write(table)
|
|
1017
|
-
|
|
1018
|
-
def _show_strategies(self, chat: ChatLog):
|
|
1019
|
-
strategies = get_available_strategies()
|
|
1020
|
-
table = Table(title=f"{SIGMA} Strategies", show_header=True, border_style="dim")
|
|
1021
|
-
table.add_column("Name", style="cyan")
|
|
1022
|
-
table.add_column("Description")
|
|
1023
|
-
for k, v in strategies.items():
|
|
1024
|
-
table.add_row(k, v.get('description', ''))
|
|
1025
|
-
chat.write(table)
|
|
1026
|
-
|
|
1027
|
-
def _switch_provider(self, provider: str, chat: ChatLog):
|
|
1028
|
-
valid = ["google", "openai", "anthropic", "groq", "xai", "ollama"]
|
|
1029
|
-
if provider not in valid:
|
|
1030
|
-
chat.write_error(f"Invalid. Use: {', '.join(valid)}")
|
|
1031
|
-
return
|
|
291
|
+
# Highlight tickers
|
|
1032
292
|
try:
|
|
1033
|
-
self.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
293
|
+
req = self.router.parse(query)
|
|
294
|
+
for ticker in req.tickers:
|
|
295
|
+
escaped_ticker = re.escape(ticker)
|
|
296
|
+
# Apply green highlight
|
|
297
|
+
display_query = re.sub(
|
|
298
|
+
f"(?i)\\b{escaped_ticker}\\b",
|
|
299
|
+
f"[bold #22c55e]{ticker.upper()}[/]",
|
|
300
|
+
display_query
|
|
301
|
+
)
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
# Write to log
|
|
306
|
+
try:
|
|
307
|
+
chat_log.write(
|
|
308
|
+
Text.assemble(
|
|
309
|
+
("❯ ", "bold #d97757"),
|
|
310
|
+
Text.from_markup(display_query)
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
chat_log.write("") # Add spacing
|
|
1038
314
|
except Exception as e:
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
def _switch_model(self, model: str, chat: ChatLog):
|
|
1042
|
-
self.settings.default_model = model
|
|
1043
|
-
self._init_llm()
|
|
1044
|
-
chat.write_system(f"Model: {model}")
|
|
1045
|
-
|
|
1046
|
-
def _set_key(self, provider: str, key: str, chat: ChatLog):
|
|
1047
|
-
"""Save an API key for a provider."""
|
|
1048
|
-
# Normalize provider name
|
|
1049
|
-
provider = provider.lower().strip()
|
|
1050
|
-
valid_providers = ["google", "openai", "anthropic", "groq", "xai", "polygon", "alphavantage", "exa"]
|
|
315
|
+
self.notify(f"Display Error: {e}", severity="error")
|
|
316
|
+
chat_log.write(f"❯ {query}")
|
|
1051
317
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
318
|
+
# Pass the specific chat_log instance to the worker to avoid re-querying failure
|
|
319
|
+
self.run_query(query, chat_log)
|
|
320
|
+
|
|
321
|
+
@work
|
|
322
|
+
async def run_query(self, query: str, chat_log: RichLog):
|
|
323
|
+
# Trace log logic simplified
|
|
324
|
+
trace_log = None
|
|
325
|
+
try:
|
|
326
|
+
# Try to find trace log on the same screen as chat_log
|
|
327
|
+
trace_log = chat_log.screen.query_one("#trace-log", RichLog)
|
|
328
|
+
except:
|
|
329
|
+
pass
|
|
1056
330
|
|
|
1057
|
-
#
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
331
|
+
# UI Animation: Thinking
|
|
332
|
+
try:
|
|
333
|
+
thinker = chat_log.screen.query_one(ThinkingStatus)
|
|
334
|
+
thinker.start()
|
|
335
|
+
except:
|
|
336
|
+
thinker = None
|
|
1062
337
|
|
|
1063
338
|
try:
|
|
1064
|
-
|
|
1065
|
-
if
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
masked = key[:6] + "..." + key[-4:] if len(key) > 10 else "***"
|
|
1071
|
-
chat.write_system(f"[green][ok][/green] {SIGMA} Key saved for [bold]{provider}[/bold]: {masked}")
|
|
1072
|
-
|
|
1073
|
-
# Auto-switch to this provider if it's an LLM provider and we don't have an LLM
|
|
1074
|
-
llm_providers = ["google", "openai", "anthropic", "groq", "xai"]
|
|
1075
|
-
if provider in llm_providers:
|
|
1076
|
-
if not self.llm or provider == getattr(self.settings.default_provider, 'value', ''):
|
|
1077
|
-
self._switch_provider(provider, chat)
|
|
339
|
+
req = self.router.parse(query)
|
|
340
|
+
if trace_log:
|
|
341
|
+
trace_log.write(f"[dim]Action:[/dim] {req.action}")
|
|
342
|
+
|
|
343
|
+
if req.is_command:
|
|
344
|
+
await self.handle_command(req, chat_log, trace_log)
|
|
1078
345
|
else:
|
|
1079
|
-
|
|
346
|
+
await self.handle_chat(req, chat_log, trace_log)
|
|
347
|
+
|
|
1080
348
|
except Exception as e:
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
349
|
+
chat_log.write(f"[red]Error: {e}[/red]")
|
|
350
|
+
|
|
351
|
+
finally:
|
|
352
|
+
if thinker:
|
|
353
|
+
thinker.stop()
|
|
354
|
+
|
|
355
|
+
async def handle_command(self, req: Request, chat_log: RichLog, trace_log: Optional[RichLog]):
|
|
356
|
+
if req.action == "backtest":
|
|
357
|
+
chat_log.write("[dim]Running backtest...[/dim]")
|
|
358
|
+
try:
|
|
359
|
+
symbol = req.tickers[0] if req.tickers else "SPY"
|
|
360
|
+
strategy = req.details.get("strategy") or "momentum"
|
|
361
|
+
from .backtest import run_backtest
|
|
362
|
+
|
|
363
|
+
result = await asyncio.to_thread(run_backtest, symbol, strategy, "1y")
|
|
364
|
+
|
|
365
|
+
if "error" in result:
|
|
366
|
+
chat_log.write(f"[red]Failed: {result['error']}[/red]")
|
|
367
|
+
else:
|
|
368
|
+
perf = result.get("performance", {})
|
|
369
|
+
# Minimalist Table
|
|
370
|
+
table = Table(box=None, show_header=False, padding=(0, 2))
|
|
371
|
+
table.add_row("[bold]Return[/]", f"[green]{perf.get('total_return')}[/]")
|
|
372
|
+
table.add_row("[bold]Sharpe[/]", f"{result.get('risk', {}).get('sharpe_ratio')}")
|
|
373
|
+
table.add_row("[bold]Equity[/]", f"{perf.get('final_equity')}")
|
|
374
|
+
|
|
375
|
+
chat_log.write(Panel(table, title=f"Backtest: {symbol}", border_style="#d97757"))
|
|
376
|
+
except Exception as e:
|
|
377
|
+
chat_log.write(f"[red]{e}[/red]")
|
|
378
|
+
|
|
379
|
+
async def handle_chat(self, req: Request, chat_log: RichLog, trace_log: Optional[RichLog]):
|
|
380
|
+
if not self.llm_router:
|
|
381
|
+
chat_log.write("[red]Setup required.[/red]")
|
|
1087
382
|
return
|
|
383
|
+
|
|
384
|
+
chat_log.write("") # break
|
|
385
|
+
|
|
386
|
+
# System prompt to enforce thought signatures for Hack Club/Gemini compatibility
|
|
387
|
+
system_prompt = (
|
|
388
|
+
"You are Sigma, an advanced financial research assistant. "
|
|
389
|
+
"You are powered by various LLMs and have access to real-time financial tools. "
|
|
390
|
+
"You MUST use available tools for any question regarding stock prices, financials, market news, or analysis. "
|
|
391
|
+
"Never hallucinate financial data. Always fetch it using the provided tools. "
|
|
392
|
+
"CRITICAL INSTRUCTION: When calling any tool, you MUST provide the 'thought_signature' parameter. "
|
|
393
|
+
"This parameter should contain your internal reasoning for why you are calling that specific tool. "
|
|
394
|
+
"Do not output the thought signature in your final response, only use it in the tool call. "
|
|
395
|
+
"Format your final response in clean Markdown."
|
|
396
|
+
)
|
|
1088
397
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
# Clear ticker highlight and start sigma animation
|
|
1095
|
-
ticker_highlight.update("")
|
|
1096
|
-
indicator.set_active(True, mode="thinking")
|
|
398
|
+
messages = [
|
|
399
|
+
{"role": "system", "content": system_prompt},
|
|
400
|
+
{"role": "user", "content": req.original_query}
|
|
401
|
+
]
|
|
1097
402
|
|
|
1098
403
|
try:
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
async def on_tool(name: str, args: dict):
|
|
1106
|
-
tool_display.add_tool_call(name)
|
|
1107
|
-
if name == "run_backtest":
|
|
1108
|
-
result = run_backtest(**args)
|
|
1109
|
-
else:
|
|
1110
|
-
result = execute_tool(name, args)
|
|
1111
|
-
tool_display.complete_tool_call(name)
|
|
1112
|
-
return result
|
|
1113
|
-
|
|
1114
|
-
response = await self.llm.generate(messages, tools=all_tools, on_tool_call=on_tool)
|
|
1115
|
-
|
|
1116
|
-
# Clear tool display after getting response
|
|
1117
|
-
await asyncio.sleep(0.5) # Brief pause to show completion
|
|
1118
|
-
tool_display.clear()
|
|
404
|
+
stream = await self.llm_router.chat(
|
|
405
|
+
messages,
|
|
406
|
+
tools=TOOL_REGISTRY.to_llm_format(),
|
|
407
|
+
on_tool_call=self.mk_tool_callback(trace_log)
|
|
408
|
+
)
|
|
1119
409
|
|
|
1120
|
-
if
|
|
1121
|
-
|
|
1122
|
-
self.conversation.append({"role": "user", "content": query})
|
|
1123
|
-
self.conversation.append({"role": "assistant", "content": response})
|
|
1124
|
-
if len(self.conversation) > 20:
|
|
1125
|
-
self.conversation = self.conversation[-20:]
|
|
410
|
+
if isinstance(stream, str):
|
|
411
|
+
chat_log.write(Markdown(stream))
|
|
1126
412
|
else:
|
|
1127
|
-
|
|
413
|
+
acc = ""
|
|
414
|
+
async for chunk in stream:
|
|
415
|
+
acc += chunk
|
|
416
|
+
chat_log.write(Markdown(acc))
|
|
1128
417
|
|
|
1129
|
-
except SigmaError as e:
|
|
1130
|
-
tool_display.clear()
|
|
1131
|
-
# Format SigmaError nicely
|
|
1132
|
-
chat.write_error(f"[E{e.code}] {e.message}")
|
|
1133
|
-
if e.details.get("hint"):
|
|
1134
|
-
chat.write_system(f"[dim]Hint: {e.details['hint']}[/dim]")
|
|
1135
418
|
except Exception as e:
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
def action_help_toggle(self):
|
|
1160
|
-
"""Toggle quick help panel."""
|
|
1161
|
-
help_panel = self.query_one("#help-panel", Static)
|
|
1162
|
-
if self.show_help:
|
|
1163
|
-
help_panel.remove_class("visible")
|
|
1164
|
-
help_panel.update("")
|
|
1165
|
-
else:
|
|
1166
|
-
help_panel.add_class("visible")
|
|
1167
|
-
help_panel.update(Text.from_markup(
|
|
1168
|
-
"[bold]Quick Commands:[/bold] /help /clear /keys /models /status /backtest "
|
|
1169
|
-
"[bold]Shortcuts:[/bold] Tab=autocomplete Ctrl+L=clear Ctrl+M=models"
|
|
1170
|
-
))
|
|
1171
|
-
self.show_help = not self.show_help
|
|
1172
|
-
|
|
1173
|
-
def action_cancel(self):
|
|
1174
|
-
if self.is_processing:
|
|
1175
|
-
self.is_processing = False
|
|
1176
|
-
tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
|
|
1177
|
-
tool_display.clear()
|
|
1178
|
-
|
|
419
|
+
chat_log.write(f"[red]LLM Error: {e}[/red]")
|
|
420
|
+
|
|
421
|
+
def mk_tool_callback(self, trace_log: Optional[RichLog]):
|
|
422
|
+
async def callback(name: str, args: dict):
|
|
423
|
+
if trace_log:
|
|
424
|
+
trace_log.write(f"[yellow]➜ {name}[/yellow]")
|
|
425
|
+
try:
|
|
426
|
+
res = await TOOL_REGISTRY.execute(name, args)
|
|
427
|
+
if trace_log:
|
|
428
|
+
trace_log.write(f"[green]✔[/green]")
|
|
429
|
+
return res
|
|
430
|
+
except Exception as e:
|
|
431
|
+
if trace_log:
|
|
432
|
+
trace_log.write(f"[red]✖ {e}[/red]")
|
|
433
|
+
raise
|
|
434
|
+
return callback
|
|
435
|
+
|
|
436
|
+
def action_toggle_sidebar(self):
|
|
437
|
+
try:
|
|
438
|
+
sidebar = self.query_one("#sidebar")
|
|
439
|
+
sidebar.set_class(not sidebar.has_class("visible"), "visible")
|
|
440
|
+
except:
|
|
441
|
+
pass
|
|
1179
442
|
|
|
1180
443
|
def launch():
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
if __name__ == "__main__":
|
|
1186
|
-
launch()
|
|
444
|
+
app = SigmaApp()
|
|
445
|
+
app.run()
|