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