sigma-terminal 2.0.2__py3-none-any.whl → 3.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigma/__init__.py +182 -6
- sigma/__main__.py +2 -2
- sigma/analytics/__init__.py +636 -0
- sigma/app.py +801 -892
- sigma/backtest.py +372 -0
- sigma/charts.py +407 -0
- sigma/cli.py +465 -0
- sigma/comparison.py +611 -0
- sigma/config.py +366 -0
- sigma/core/__init__.py +4 -17
- sigma/core/engine.py +493 -0
- sigma/core/intent.py +595 -0
- sigma/core/models.py +516 -125
- sigma/data/__init__.py +681 -0
- sigma/data/models.py +130 -0
- sigma/llm.py +639 -0
- sigma/monitoring.py +666 -0
- sigma/portfolio.py +697 -0
- sigma/reporting.py +658 -0
- sigma/robustness.py +675 -0
- sigma/setup.py +374 -403
- sigma/strategy.py +753 -0
- sigma/tools/backtest.py +23 -5
- sigma/tools.py +617 -0
- sigma/visualization.py +766 -0
- sigma_terminal-3.3.0.dist-info/METADATA +583 -0
- sigma_terminal-3.3.0.dist-info/RECORD +30 -0
- sigma_terminal-3.3.0.dist-info/entry_points.txt +6 -0
- sigma_terminal-3.3.0.dist-info/licenses/LICENSE +25 -0
- sigma/core/agent.py +0 -205
- sigma/core/config.py +0 -119
- sigma/core/llm.py +0 -794
- sigma/tools/__init__.py +0 -5
- sigma/tools/charts.py +0 -400
- sigma/tools/financial.py +0 -1457
- sigma/ui/__init__.py +0 -1
- sigma_terminal-2.0.2.dist-info/METADATA +0 -222
- sigma_terminal-2.0.2.dist-info/RECORD +0 -19
- sigma_terminal-2.0.2.dist-info/entry_points.txt +0 -2
- sigma_terminal-2.0.2.dist-info/licenses/LICENSE +0 -42
- {sigma_terminal-2.0.2.dist-info → sigma_terminal-3.3.0.dist-info}/WHEEL +0 -0
sigma/app.py
CHANGED
|
@@ -1,947 +1,856 @@
|
|
|
1
|
-
"""Sigma
|
|
1
|
+
"""Sigma v3.3.0 - Finance Research Agent."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import
|
|
4
|
+
import os
|
|
5
5
|
import re
|
|
6
|
-
from
|
|
7
|
-
import
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, List
|
|
8
8
|
|
|
9
|
-
from rich.
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
10
|
from rich.panel import Panel
|
|
11
|
-
from rich.text import Text
|
|
12
11
|
from rich.table import Table
|
|
13
|
-
from rich.
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from textual import on, work
|
|
14
|
+
from textual.app import App, ComposeResult
|
|
15
|
+
from textual.binding import Binding
|
|
16
|
+
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
17
|
+
from textual.widgets import Footer, Input, RichLog, Static
|
|
18
|
+
from textual.suggester import Suggester
|
|
19
|
+
|
|
20
|
+
from .config import LLMProvider, get_settings, save_api_key, AVAILABLE_MODELS
|
|
21
|
+
from .llm import get_llm
|
|
22
|
+
from .tools import TOOLS, execute_tool
|
|
23
|
+
from .backtest import run_backtest, get_available_strategies, BACKTEST_TOOL
|
|
24
|
+
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
__version__ = "3.3.0"
|
|
27
|
+
SIGMA = "σ"
|
|
21
28
|
|
|
29
|
+
# Common stock tickers for recognition
|
|
30
|
+
COMMON_TICKERS = {
|
|
31
|
+
"AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "META", "TSLA", "BRK.A", "BRK.B",
|
|
32
|
+
"JPM", "JNJ", "V", "PG", "UNH", "HD", "MA", "DIS", "PYPL", "BAC", "ADBE", "NFLX",
|
|
33
|
+
"CRM", "INTC", "AMD", "CSCO", "PEP", "KO", "ABT", "NKE", "MRK", "PFE", "TMO",
|
|
34
|
+
"COST", "AVGO", "WMT", "ACN", "LLY", "MCD", "DHR", "TXN", "NEE", "PM", "HON",
|
|
35
|
+
"UPS", "BMY", "QCOM", "LOW", "MS", "RTX", "UNP", "ORCL", "IBM", "GE", "CAT",
|
|
36
|
+
"SBUX", "AMAT", "GS", "BLK", "DE", "AMT", "NOW", "ISRG", "LMT", "MDLZ", "AXP",
|
|
37
|
+
"SYK", "BKNG", "PLD", "GILD", "ADI", "TMUS", "CVS", "MMC", "ZTS", "CB", "C",
|
|
38
|
+
"SPY", "QQQ", "IWM", "DIA", "VTI", "VOO", "VXX", "ARKK", "XLF", "XLK", "XLE",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Small sigma animation frames (minimal footprint)
|
|
42
|
+
SMALL_SIGMA_FRAMES = [
|
|
43
|
+
"[bold blue]σ[/bold blue]",
|
|
44
|
+
"[bold cyan]σ[/bold cyan]",
|
|
45
|
+
"[bold white]σ[/bold white]",
|
|
46
|
+
"[bold #60a5fa]σ[/bold #60a5fa]",
|
|
47
|
+
]
|
|
22
48
|
|
|
23
|
-
|
|
49
|
+
# Tool call animation frames
|
|
50
|
+
TOOL_CALL_FRAMES = [
|
|
51
|
+
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
|
|
52
|
+
]
|
|
24
53
|
|
|
25
|
-
#
|
|
26
|
-
|
|
54
|
+
# Welcome banner - clean design
|
|
55
|
+
WELCOME_BANNER = """
|
|
56
|
+
[bold blue]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold blue]
|
|
57
|
+
[bold blue]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold blue]
|
|
58
|
+
[bold blue]███████╗██║██║ ███╗██╔████╔██║███████║[/bold blue]
|
|
59
|
+
[bold blue]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold blue]
|
|
60
|
+
[bold blue]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold blue]
|
|
61
|
+
[bold blue]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold blue]
|
|
27
62
|
|
|
28
|
-
|
|
29
|
-
BANNER = """[bold bright_blue]
|
|
30
|
-
███████╗██╗ ██████╗ ███╗ ███╗ █████╗
|
|
31
|
-
██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗
|
|
32
|
-
███████╗██║██║ ███╗██╔████╔██║███████║
|
|
33
|
-
╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║
|
|
34
|
-
███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║
|
|
35
|
-
╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold bright_blue]
|
|
63
|
+
[bold cyan]Finance Research Agent[/bold cyan] [dim]v3.3.0 | Native macOS[/dim]
|
|
36
64
|
"""
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
[bold white] Institutional-Grade Financial Research Agent[/bold white]
|
|
40
|
-
[dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/dim]"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def print_banner(model: str):
|
|
44
|
-
"""Print the startup banner."""
|
|
45
|
-
console.print()
|
|
46
|
-
console.print(BANNER)
|
|
47
|
-
console.print(SUB_BANNER)
|
|
48
|
-
console.print()
|
|
49
|
-
|
|
50
|
-
# Status line
|
|
51
|
-
console.print(f" [dim]Version:[/dim] [bright_cyan]{VERSION}[/bright_cyan] [dim]Model:[/dim] [bright_cyan]{model}[/bright_cyan]")
|
|
52
|
-
console.print()
|
|
53
|
-
console.print(" [dim]Type[/dim] [bold bright_yellow]/help[/bold bright_yellow] [dim]for commands[/dim] [dim]Type[/dim] [bold bright_yellow]/quit[/bold bright_yellow] [dim]to exit[/dim]")
|
|
54
|
-
console.print()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def print_help():
|
|
58
|
-
"""Print comprehensive help."""
|
|
59
|
-
console.print()
|
|
60
|
-
|
|
61
|
-
# Commands table
|
|
62
|
-
commands = Table(title="[bold bright_cyan]Commands[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
63
|
-
commands.add_column("Command", style="bright_yellow")
|
|
64
|
-
commands.add_column("Description", style="white")
|
|
65
|
-
|
|
66
|
-
commands.add_row("/help, /h, /?", "Show this help message")
|
|
67
|
-
commands.add_row("/model <provider>", "Switch LLM provider (openai, anthropic, google, groq, ollama)")
|
|
68
|
-
commands.add_row("/mode <mode>", "Switch analysis mode (default, technical, fundamental, quant)")
|
|
69
|
-
commands.add_row("/clear", "Clear conversation history")
|
|
70
|
-
commands.add_row("/status", "Show current configuration")
|
|
71
|
-
commands.add_row("/chart <symbol>", "Quick price chart for a symbol")
|
|
72
|
-
commands.add_row("/compare <sym1> <sym2>...", "Quick comparison chart")
|
|
73
|
-
commands.add_row("/backtest <symbol> <strategy>", "Generate backtest algorithm")
|
|
74
|
-
commands.add_row("/lean setup", "Setup LEAN Engine for backtesting")
|
|
75
|
-
commands.add_row("/lean run <symbol> <strategy>", "Run backtest with LEAN Engine")
|
|
76
|
-
commands.add_row("/quit, /exit, /q", "Exit Sigma")
|
|
77
|
-
|
|
78
|
-
console.print(commands)
|
|
79
|
-
console.print()
|
|
80
|
-
|
|
81
|
-
# Analysis modes
|
|
82
|
-
modes = Table(title="[bold bright_cyan]Analysis Modes[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
83
|
-
modes.add_column("Mode", style="bright_yellow")
|
|
84
|
-
modes.add_column("Description", style="white")
|
|
85
|
-
|
|
86
|
-
modes.add_row("default", "Comprehensive analysis using all available tools")
|
|
87
|
-
modes.add_row("technical", "Focus on technical indicators, charts, and price action")
|
|
88
|
-
modes.add_row("fundamental", "Focus on financial statements, ratios, and valuations")
|
|
89
|
-
modes.add_row("quant", "Quantitative analysis with predictions and backtesting")
|
|
90
|
-
|
|
91
|
-
console.print(modes)
|
|
92
|
-
console.print()
|
|
93
|
-
|
|
94
|
-
# Examples
|
|
95
|
-
examples = Table(title="[bold bright_cyan]Example Queries[/bold bright_cyan]", box=ROUNDED, show_header=False)
|
|
96
|
-
examples.add_column("Query", style="white")
|
|
97
|
-
|
|
98
|
-
examples.add_row("[dim]── Stock Analysis ──[/dim]")
|
|
99
|
-
examples.add_row(" Analyze NVDA stock")
|
|
100
|
-
examples.add_row(" Is AAPL a good investment?")
|
|
101
|
-
examples.add_row(" Give me a deep dive on Tesla")
|
|
102
|
-
examples.add_row("")
|
|
103
|
-
examples.add_row("[dim]── Charts & Visualization ──[/dim]")
|
|
104
|
-
examples.add_row(" Show me a chart of MSFT for the past 6 months")
|
|
105
|
-
examples.add_row(" Compare NVDA, AMD, and INTC")
|
|
106
|
-
examples.add_row(" Show RSI chart for SPY")
|
|
107
|
-
examples.add_row("")
|
|
108
|
-
examples.add_row("[dim]── Technical Analysis ──[/dim]")
|
|
109
|
-
examples.add_row(" Technical analysis on QQQ")
|
|
110
|
-
examples.add_row(" What are the support levels for META?")
|
|
111
|
-
examples.add_row(" Is GOOGL overbought?")
|
|
112
|
-
examples.add_row("")
|
|
113
|
-
examples.add_row("[dim]── Predictions & Sentiment ──[/dim]")
|
|
114
|
-
examples.add_row(" Predict AMZN price for next month")
|
|
115
|
-
examples.add_row(" What's the sentiment on TSLA?")
|
|
116
|
-
examples.add_row(" Price forecast for Bitcoin")
|
|
117
|
-
examples.add_row("")
|
|
118
|
-
examples.add_row("[dim]── Backtesting & Strategy ──[/dim]")
|
|
119
|
-
examples.add_row(" Create a MACD strategy backtest for AAPL")
|
|
120
|
-
examples.add_row(" Generate RSI mean reversion backtest for SPY")
|
|
121
|
-
examples.add_row(" List available backtest strategies")
|
|
122
|
-
examples.add_row("")
|
|
123
|
-
examples.add_row("[dim]── Market Overview ──[/dim]")
|
|
124
|
-
examples.add_row(" What are today's market movers?")
|
|
125
|
-
examples.add_row(" Show sector performance")
|
|
126
|
-
examples.add_row(" How are the major indices doing?")
|
|
127
|
-
|
|
128
|
-
console.print(examples)
|
|
129
|
-
console.print()
|
|
130
|
-
|
|
131
|
-
# Strategies
|
|
132
|
-
strategies = Table(title="[bold bright_cyan]Backtest Strategies[/bold bright_cyan]", box=ROUNDED, show_header=True, header_style="bold")
|
|
133
|
-
strategies.add_column("Strategy", style="bright_yellow")
|
|
134
|
-
strategies.add_column("Description", style="white")
|
|
135
|
-
|
|
136
|
-
strategies.add_row("sma_crossover", "Classic moving average crossover (fast/slow SMA)")
|
|
137
|
-
strategies.add_row("rsi_mean_reversion", "Buy oversold, sell overbought using RSI")
|
|
138
|
-
strategies.add_row("macd_momentum", "MACD histogram momentum strategy")
|
|
139
|
-
strategies.add_row("bollinger_bands", "Mean reversion at Bollinger Band extremes")
|
|
140
|
-
strategies.add_row("dual_momentum", "Absolute + relative momentum")
|
|
141
|
-
strategies.add_row("breakout", "Donchian channel breakout strategy")
|
|
142
|
-
|
|
143
|
-
console.print(strategies)
|
|
144
|
-
console.print()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# Tool name mappings for cleaner display
|
|
148
|
-
TOOL_DISPLAY_NAMES = {
|
|
149
|
-
"get_stock_quote": "Stock Quote",
|
|
150
|
-
"get_stock_history": "Price History",
|
|
151
|
-
"get_company_info": "Company Info",
|
|
152
|
-
"get_financial_statements": "Financials",
|
|
153
|
-
"get_analyst_recommendations": "Analyst Ratings",
|
|
154
|
-
"get_insider_trades": "Insider Trades",
|
|
155
|
-
"get_institutional_holders": "Institutions",
|
|
156
|
-
"get_earnings_calendar": "Earnings",
|
|
157
|
-
"get_options_chain": "Options Chain",
|
|
158
|
-
"get_dividends": "Dividends",
|
|
159
|
-
"compare_stocks": "Compare Stocks",
|
|
160
|
-
"get_market_movers": "Market Movers",
|
|
161
|
-
"get_sector_performance": "Sectors",
|
|
162
|
-
"get_market_indices": "Market Indices",
|
|
163
|
-
"calculate_portfolio_metrics": "Portfolio",
|
|
164
|
-
"search_stocks": "Stock Search",
|
|
165
|
-
"get_stock_news": "News",
|
|
166
|
-
"technical_analysis": "Technicals",
|
|
167
|
-
"generate_price_chart": "Price Chart",
|
|
168
|
-
"generate_comparison_chart": "Comparison",
|
|
169
|
-
"generate_rsi_chart": "RSI Chart",
|
|
170
|
-
"generate_sector_chart": "Sectors",
|
|
171
|
-
"list_backtest_strategies": "Strategies",
|
|
172
|
-
"generate_backtest": "Backtest",
|
|
173
|
-
"generate_custom_backtest": "Custom Backtest",
|
|
174
|
-
"price_forecast": "Forecast",
|
|
175
|
-
"sentiment_analysis": "Sentiment",
|
|
176
|
-
}
|
|
66
|
+
SYSTEM_PROMPT = """You are Sigma, a Finance Research Agent. You provide comprehensive market analysis, trading strategies, and investment insights.
|
|
177
67
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"get_earnings_calendar": ">",
|
|
187
|
-
"get_options_chain": ">",
|
|
188
|
-
"get_dividends": ">",
|
|
189
|
-
"compare_stocks": ">",
|
|
190
|
-
"get_market_movers": ">",
|
|
191
|
-
"get_sector_performance": ">",
|
|
192
|
-
"get_market_indices": ">",
|
|
193
|
-
"calculate_portfolio_metrics": ">",
|
|
194
|
-
"search_stocks": ">",
|
|
195
|
-
"get_stock_news": ">",
|
|
196
|
-
"technical_analysis": ">",
|
|
197
|
-
"generate_price_chart": ">",
|
|
198
|
-
"generate_comparison_chart": ">",
|
|
199
|
-
"generate_rsi_chart": ">",
|
|
200
|
-
"generate_sector_chart": ">",
|
|
201
|
-
"list_backtest_strategies": ">",
|
|
202
|
-
"generate_backtest": ">",
|
|
203
|
-
"generate_custom_backtest": ">",
|
|
204
|
-
"price_forecast": ">",
|
|
205
|
-
"sentiment_analysis": ">",
|
|
206
|
-
}
|
|
68
|
+
CORE CAPABILITIES:
|
|
69
|
+
- Real-time market data analysis (quotes, charts, technicals)
|
|
70
|
+
- Fundamental analysis (financials, ratios, earnings)
|
|
71
|
+
- Technical analysis (RSI, MACD, Bollinger Bands, moving averages)
|
|
72
|
+
- Backtesting strategies (SMA crossover, RSI, MACD, Bollinger, momentum, breakout)
|
|
73
|
+
- Portfolio analysis and optimization
|
|
74
|
+
- Sector and market overview
|
|
75
|
+
- Insider and fund activity tracking
|
|
207
76
|
|
|
77
|
+
RESPONSE STYLE:
|
|
78
|
+
- Be concise and data-driven
|
|
79
|
+
- Lead with key insights, then supporting data
|
|
80
|
+
- Use tables for comparative data when appropriate
|
|
81
|
+
- Always cite specific numbers and metrics
|
|
82
|
+
- Provide actionable recommendations when asked
|
|
83
|
+
- Format currency and percentages properly
|
|
84
|
+
- Use STRONG BUY, BUY, HOLD, SELL, STRONG SELL ratings when appropriate
|
|
208
85
|
|
|
209
|
-
|
|
210
|
-
"""Format tool call with description. Returns (display_name, detail)."""
|
|
211
|
-
display_name = TOOL_DISPLAY_NAMES.get(name, name.replace('_', ' ').title())
|
|
212
|
-
icon = TOOL_ICONS.get(name, "●")
|
|
213
|
-
|
|
214
|
-
symbol = args.get('symbol', '')
|
|
215
|
-
symbols = args.get('symbols', [])
|
|
216
|
-
query = args.get('query', '')
|
|
217
|
-
strategy = args.get('strategy', '')
|
|
218
|
-
|
|
219
|
-
if symbol and strategy:
|
|
220
|
-
return f"{icon} {display_name}", f"{symbol.upper()} - {strategy}"
|
|
221
|
-
elif symbol:
|
|
222
|
-
return f"{icon} {display_name}", symbol.upper()
|
|
223
|
-
elif symbols:
|
|
224
|
-
return f"{icon} {display_name}", ', '.join(s.upper() for s in symbols[:3])
|
|
225
|
-
elif query:
|
|
226
|
-
return f"{icon} {display_name}", query[:30]
|
|
227
|
-
else:
|
|
228
|
-
return f"{icon} {display_name}", None
|
|
86
|
+
When users ask about stocks, always gather current data using your tools before responding."""
|
|
229
87
|
|
|
88
|
+
# Enhanced autocomplete suggestions with more variety
|
|
89
|
+
SUGGESTIONS = [
|
|
90
|
+
# Analysis commands
|
|
91
|
+
"analyze AAPL",
|
|
92
|
+
"analyze MSFT",
|
|
93
|
+
"analyze GOOGL",
|
|
94
|
+
"analyze NVDA",
|
|
95
|
+
"analyze TSLA",
|
|
96
|
+
"analyze META",
|
|
97
|
+
"analyze AMZN",
|
|
98
|
+
"analyze AMD",
|
|
99
|
+
"analyze SPY",
|
|
100
|
+
# Comparisons
|
|
101
|
+
"compare AAPL MSFT GOOGL",
|
|
102
|
+
"compare NVDA AMD INTC",
|
|
103
|
+
"compare META GOOGL AMZN",
|
|
104
|
+
"compare TSLA RIVN LCID",
|
|
105
|
+
# Technical
|
|
106
|
+
"technical analysis of AAPL",
|
|
107
|
+
"technical analysis of SPY",
|
|
108
|
+
"technical analysis of NVDA",
|
|
109
|
+
"technical analysis of QQQ",
|
|
110
|
+
# Backtesting
|
|
111
|
+
"backtest SMA crossover on AAPL",
|
|
112
|
+
"backtest RSI strategy on SPY",
|
|
113
|
+
"backtest MACD on NVDA",
|
|
114
|
+
"backtest momentum on QQQ",
|
|
115
|
+
# Market
|
|
116
|
+
"market overview",
|
|
117
|
+
"sector performance",
|
|
118
|
+
"what sectors are hot today",
|
|
119
|
+
# Quotes
|
|
120
|
+
"get quote for AAPL",
|
|
121
|
+
"price of NVDA",
|
|
122
|
+
"how is TSLA doing",
|
|
123
|
+
# Fundamentals
|
|
124
|
+
"fundamentals of MSFT",
|
|
125
|
+
"financials for AAPL",
|
|
126
|
+
"earnings of NVDA",
|
|
127
|
+
# Activity
|
|
128
|
+
"insider trading for AAPL",
|
|
129
|
+
"institutional holders of NVDA",
|
|
130
|
+
"analyst recommendations for TSLA",
|
|
131
|
+
# Natural language queries
|
|
132
|
+
"what should I know about AAPL",
|
|
133
|
+
"is NVDA overvalued",
|
|
134
|
+
"best tech stocks right now",
|
|
135
|
+
"should I buy TSLA",
|
|
136
|
+
# Commands
|
|
137
|
+
"/help",
|
|
138
|
+
"/clear",
|
|
139
|
+
"/keys",
|
|
140
|
+
"/models",
|
|
141
|
+
"/status",
|
|
142
|
+
"/backtest",
|
|
143
|
+
]
|
|
230
144
|
|
|
231
|
-
|
|
232
|
-
|
|
145
|
+
|
|
146
|
+
def extract_tickers(text: str) -> List[str]:
|
|
147
|
+
"""Extract stock tickers from text."""
|
|
148
|
+
# Look for common patterns: $AAPL, or standalone uppercase words
|
|
149
|
+
# Only match if it's a known ticker or starts with $
|
|
150
|
+
words = text.upper().split()
|
|
151
|
+
tickers = []
|
|
152
|
+
|
|
153
|
+
for word in words:
|
|
154
|
+
# Clean the word
|
|
155
|
+
clean = word.strip('.,!?()[]{}":;')
|
|
156
|
+
|
|
157
|
+
# Check for $TICKER format
|
|
158
|
+
if clean.startswith('$'):
|
|
159
|
+
ticker = clean[1:]
|
|
160
|
+
if ticker and ticker.isalpha() and len(ticker) <= 5:
|
|
161
|
+
tickers.append(ticker)
|
|
162
|
+
# Check if it's a known ticker
|
|
163
|
+
elif clean in COMMON_TICKERS:
|
|
164
|
+
tickers.append(clean)
|
|
165
|
+
|
|
166
|
+
return list(dict.fromkeys(tickers)) # Dedupe while preserving order
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SigmaSuggester(Suggester):
|
|
170
|
+
"""Enhanced autocomplete suggester with ticker recognition."""
|
|
233
171
|
|
|
234
172
|
def __init__(self):
|
|
235
|
-
|
|
236
|
-
self.agent: Optional[SigmaAgent] = None
|
|
237
|
-
self.provider = self.settings.default_provider
|
|
238
|
-
self.tool_calls: list[tuple[str, float]] = []
|
|
239
|
-
self.start_time = 0.0
|
|
240
|
-
self.custom_model: Optional[str] = None
|
|
241
|
-
self.mode = "default" # default, technical, fundamental, quant
|
|
242
|
-
self.chart_output: Optional[str] = None # Store chart for display
|
|
243
|
-
|
|
244
|
-
def _init_agent(self):
|
|
245
|
-
"""Initialize agent."""
|
|
246
|
-
self.agent = SigmaAgent(provider=self.provider, model=self.custom_model)
|
|
247
|
-
|
|
248
|
-
def _get_model_display(self) -> str:
|
|
249
|
-
"""Get model name for display."""
|
|
250
|
-
if self.custom_model:
|
|
251
|
-
return self.custom_model
|
|
252
|
-
return self.settings.get_model(self.provider)
|
|
253
|
-
|
|
254
|
-
def on_tool_start(self, name: str, args: dict):
|
|
255
|
-
"""Called when tool starts."""
|
|
256
|
-
display_name, detail = format_tool_description(name, args)
|
|
257
|
-
if detail:
|
|
258
|
-
console.print(f" [bright_cyan]{display_name}[/bright_cyan] [dim]({detail})[/dim]")
|
|
259
|
-
else:
|
|
260
|
-
console.print(f" [bright_cyan]{display_name}[/bright_cyan]")
|
|
173
|
+
super().__init__(use_cache=True, case_sensitive=False)
|
|
261
174
|
|
|
262
|
-
def
|
|
263
|
-
"""
|
|
264
|
-
|
|
175
|
+
async def get_suggestion(self, value: str) -> Optional[str]:
|
|
176
|
+
"""Get autocomplete suggestion."""
|
|
177
|
+
if not value or len(value) < 2:
|
|
178
|
+
return None
|
|
265
179
|
|
|
266
|
-
|
|
267
|
-
if isinstance(result, dict) and result.get("display_as_chart"):
|
|
268
|
-
self.chart_output = result.get("chart", "")
|
|
269
|
-
|
|
270
|
-
def on_thinking(self, content: str):
|
|
271
|
-
"""Called when agent is thinking."""
|
|
272
|
-
pass
|
|
273
|
-
|
|
274
|
-
def on_response(self, content: str):
|
|
275
|
-
"""Called with final response."""
|
|
276
|
-
pass
|
|
277
|
-
|
|
278
|
-
async def process_query(self, query: str):
|
|
279
|
-
"""Process a query."""
|
|
280
|
-
if self.agent is None:
|
|
281
|
-
self._init_agent()
|
|
180
|
+
value_lower = value.lower()
|
|
282
181
|
|
|
283
|
-
|
|
182
|
+
# Check for ticker pattern (all caps or starts with $)
|
|
183
|
+
if value.startswith("$") or value.isupper():
|
|
184
|
+
ticker = value.lstrip("$").upper()
|
|
185
|
+
for common in COMMON_TICKERS:
|
|
186
|
+
if common.startswith(ticker) and common != ticker:
|
|
187
|
+
return f"analyze {common}"
|
|
284
188
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
189
|
+
# Standard suggestions
|
|
190
|
+
for suggestion in SUGGESTIONS:
|
|
191
|
+
if suggestion.lower().startswith(value_lower):
|
|
192
|
+
return suggestion
|
|
288
193
|
|
|
289
|
-
|
|
290
|
-
|
|
194
|
+
# Try partial match in middle of suggestion
|
|
195
|
+
for suggestion in SUGGESTIONS:
|
|
196
|
+
if value_lower in suggestion.lower():
|
|
197
|
+
return suggestion
|
|
291
198
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
CSS = """
|
|
203
|
+
Screen {
|
|
204
|
+
background: #0a0a0f;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
* {
|
|
208
|
+
scrollbar-size: 1 1;
|
|
209
|
+
scrollbar-color: #3b82f6 30%;
|
|
210
|
+
scrollbar-color-hover: #60a5fa 50%;
|
|
211
|
+
scrollbar-color-active: #93c5fd 70%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#main-container {
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: 100%;
|
|
217
|
+
background: #0a0a0f;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#chat-area {
|
|
221
|
+
height: 1fr;
|
|
222
|
+
margin: 1 2;
|
|
223
|
+
background: #0a0a0f;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#chat-log {
|
|
227
|
+
background: #0a0a0f;
|
|
228
|
+
padding: 1 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#status-bar {
|
|
232
|
+
height: 3;
|
|
233
|
+
background: #0d1117;
|
|
234
|
+
border-top: solid #1a1a2e;
|
|
235
|
+
padding: 0 2;
|
|
236
|
+
dock: bottom;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#status-content {
|
|
240
|
+
width: 100%;
|
|
241
|
+
height: 100%;
|
|
242
|
+
content-align: left middle;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#thinking-indicator {
|
|
246
|
+
width: auto;
|
|
247
|
+
height: 1;
|
|
248
|
+
content-align: center middle;
|
|
249
|
+
display: none;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#thinking-indicator.visible {
|
|
253
|
+
display: block;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#tool-calls-display {
|
|
257
|
+
width: 100%;
|
|
258
|
+
height: auto;
|
|
259
|
+
max-height: 6;
|
|
260
|
+
background: #0d1117;
|
|
261
|
+
border: solid #1a1a2e;
|
|
262
|
+
margin: 0 2;
|
|
263
|
+
padding: 0 1;
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#tool-calls-display.visible {
|
|
268
|
+
display: block;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#input-area {
|
|
272
|
+
height: 5;
|
|
273
|
+
padding: 1 2;
|
|
274
|
+
background: #0d1117;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#input-row {
|
|
278
|
+
height: 3;
|
|
279
|
+
width: 100%;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#sigma-indicator {
|
|
283
|
+
width: 4;
|
|
284
|
+
height: 3;
|
|
285
|
+
content-align: center middle;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#prompt-input {
|
|
289
|
+
width: 1fr;
|
|
290
|
+
background: #1a1a2e;
|
|
291
|
+
border: solid #3b82f6;
|
|
292
|
+
color: #ffffff;
|
|
293
|
+
padding: 0 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#prompt-input:focus {
|
|
297
|
+
border: solid #60a5fa;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#prompt-input.-autocomplete {
|
|
301
|
+
border: solid #22c55e;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#ticker-highlight {
|
|
305
|
+
width: auto;
|
|
306
|
+
height: 1;
|
|
307
|
+
padding: 0 1;
|
|
308
|
+
background: transparent;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Footer {
|
|
312
|
+
background: #0d1117;
|
|
313
|
+
height: 1;
|
|
314
|
+
dock: bottom;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
Footer > .footer--highlight {
|
|
318
|
+
background: transparent;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
Footer > .footer--key {
|
|
322
|
+
background: #1a1a2e;
|
|
323
|
+
color: #f59e0b;
|
|
324
|
+
text-style: bold;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Footer > .footer--description {
|
|
328
|
+
color: #6b7280;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#help-panel {
|
|
332
|
+
width: 100%;
|
|
333
|
+
height: auto;
|
|
334
|
+
padding: 1;
|
|
335
|
+
background: #0d1117;
|
|
336
|
+
border: solid #3b82f6;
|
|
337
|
+
margin: 1 2;
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#help-panel.visible {
|
|
342
|
+
display: block;
|
|
343
|
+
}
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class ToolCallDisplay(Static):
|
|
348
|
+
"""Animated display for tool calls."""
|
|
349
|
+
|
|
350
|
+
def __init__(self, *args, **kwargs):
|
|
351
|
+
super().__init__(*args, **kwargs)
|
|
352
|
+
self.tool_calls: List[dict] = []
|
|
353
|
+
self.frame = 0
|
|
354
|
+
self.timer = None
|
|
355
|
+
|
|
356
|
+
def add_tool_call(self, name: str, status: str = "running"):
|
|
357
|
+
"""Add a tool call to the display."""
|
|
358
|
+
self.tool_calls.append({"name": name, "status": status, "frame": 0})
|
|
359
|
+
self.add_class("visible")
|
|
360
|
+
self._render()
|
|
361
|
+
if not self.timer:
|
|
362
|
+
self.timer = self.set_interval(0.1, self._animate)
|
|
363
|
+
|
|
364
|
+
def complete_tool_call(self, name: str):
|
|
365
|
+
"""Mark a tool call as complete."""
|
|
366
|
+
for tc in self.tool_calls:
|
|
367
|
+
if tc["name"] == name and tc["status"] == "running":
|
|
368
|
+
tc["status"] = "complete"
|
|
369
|
+
break
|
|
370
|
+
self._render()
|
|
318
371
|
|
|
319
|
-
def
|
|
320
|
-
"""
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
def clear(self):
|
|
373
|
+
"""Clear all tool calls."""
|
|
374
|
+
self.tool_calls = []
|
|
375
|
+
if self.timer:
|
|
376
|
+
self.timer.stop()
|
|
377
|
+
self.timer = None
|
|
378
|
+
self.remove_class("visible")
|
|
379
|
+
self.update("")
|
|
380
|
+
|
|
381
|
+
def _animate(self):
|
|
382
|
+
"""Animate the spinner."""
|
|
383
|
+
self.frame = (self.frame + 1) % len(TOOL_CALL_FRAMES)
|
|
384
|
+
for tc in self.tool_calls:
|
|
385
|
+
if tc["status"] == "running":
|
|
386
|
+
tc["frame"] = self.frame
|
|
387
|
+
self._render()
|
|
388
|
+
|
|
389
|
+
def _render(self):
|
|
390
|
+
"""Render the tool calls display."""
|
|
391
|
+
if not self.tool_calls:
|
|
392
|
+
return
|
|
324
393
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
continue
|
|
333
|
-
elif in_table and line.strip():
|
|
334
|
-
if '|' in line:
|
|
335
|
-
table_lines.append(line)
|
|
336
|
-
continue
|
|
337
|
-
else:
|
|
338
|
-
self._print_table(table_lines)
|
|
339
|
-
in_table = False
|
|
340
|
-
table_lines = []
|
|
341
|
-
elif in_table and not line.strip():
|
|
342
|
-
self._print_table(table_lines)
|
|
343
|
-
in_table = False
|
|
344
|
-
table_lines = []
|
|
345
|
-
|
|
346
|
-
# Format and print the line
|
|
347
|
-
formatted = self._format_line(line)
|
|
348
|
-
if formatted is not None:
|
|
349
|
-
console.print(formatted)
|
|
394
|
+
lines = []
|
|
395
|
+
for tc in self.tool_calls:
|
|
396
|
+
if tc["status"] == "running":
|
|
397
|
+
spinner = TOOL_CALL_FRAMES[tc["frame"]]
|
|
398
|
+
lines.append(f" [cyan]{spinner}[/cyan] [bold]{tc['name']}[/bold] [dim]executing...[/dim]")
|
|
399
|
+
else:
|
|
400
|
+
lines.append(f" [green]✓[/green] [bold]{tc['name']}[/bold] [green]complete[/green]")
|
|
350
401
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
402
|
+
self.update(Text.from_markup("\n".join(lines)))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class SigmaIndicator(Static):
|
|
406
|
+
"""Pulsing sigma indicator with minimal footprint."""
|
|
407
|
+
|
|
408
|
+
def __init__(self, *args, **kwargs):
|
|
409
|
+
super().__init__(*args, **kwargs)
|
|
410
|
+
self.active = False
|
|
411
|
+
self.frame = 0
|
|
412
|
+
self.timer = None
|
|
413
|
+
|
|
414
|
+
def on_mount(self):
|
|
415
|
+
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
416
|
+
|
|
417
|
+
def set_active(self, active: bool):
|
|
418
|
+
self.active = active
|
|
419
|
+
if active and not self.timer:
|
|
420
|
+
self.timer = self.set_interval(0.15, self._pulse)
|
|
421
|
+
elif not active and self.timer:
|
|
422
|
+
self.timer.stop()
|
|
423
|
+
self.timer = None
|
|
424
|
+
self.update(Text.from_markup(f"[bold blue]{SIGMA}[/bold blue]"))
|
|
425
|
+
|
|
426
|
+
def _pulse(self):
|
|
427
|
+
self.frame = (self.frame + 1) % len(SMALL_SIGMA_FRAMES)
|
|
428
|
+
self.update(Text.from_markup(SMALL_SIGMA_FRAMES[self.frame]))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TickerHighlight(Static):
|
|
432
|
+
"""Display detected tickers in real-time."""
|
|
433
|
+
|
|
434
|
+
def update_tickers(self, text: str):
|
|
435
|
+
"""Update displayed tickers based on input."""
|
|
436
|
+
tickers = extract_tickers(text)
|
|
437
|
+
if tickers:
|
|
438
|
+
ticker_text = " ".join([f"[cyan]${t}[/cyan]" for t in tickers[:3]])
|
|
439
|
+
self.update(Text.from_markup(ticker_text))
|
|
440
|
+
else:
|
|
441
|
+
self.update("")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class ChatLog(RichLog):
|
|
445
|
+
"""Chat log with rich formatting."""
|
|
446
|
+
|
|
447
|
+
def write_user(self, message: str):
|
|
448
|
+
# Highlight any tickers in user message
|
|
449
|
+
highlighted = message
|
|
450
|
+
for ticker in extract_tickers(message):
|
|
451
|
+
highlighted = re.sub(
|
|
452
|
+
rf'\b{ticker}\b',
|
|
453
|
+
f'[cyan]{ticker}[/cyan]',
|
|
454
|
+
highlighted,
|
|
455
|
+
flags=re.IGNORECASE
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
self.write(Panel(
|
|
459
|
+
Text.from_markup(highlighted) if '[cyan]' in highlighted else Text(message, style="white"),
|
|
460
|
+
title="[bold blue]You[/bold blue]",
|
|
461
|
+
border_style="blue",
|
|
462
|
+
padding=(0, 1),
|
|
463
|
+
))
|
|
464
|
+
|
|
465
|
+
def write_assistant(self, message: str):
|
|
466
|
+
self.write(Panel(
|
|
467
|
+
Markdown(message),
|
|
468
|
+
title=f"[bold cyan]{SIGMA} Sigma[/bold cyan]",
|
|
469
|
+
border_style="cyan",
|
|
470
|
+
padding=(0, 1),
|
|
471
|
+
))
|
|
472
|
+
|
|
473
|
+
def write_tool(self, tool_name: str):
|
|
474
|
+
# This is now handled by ToolCallDisplay
|
|
475
|
+
pass
|
|
354
476
|
|
|
355
|
-
def
|
|
356
|
-
"""
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
477
|
+
def write_error(self, message: str):
|
|
478
|
+
self.write(Panel(Text(message, style="red"), title="[red]Error[/red]", border_style="red"))
|
|
479
|
+
|
|
480
|
+
def write_system(self, message: str):
|
|
481
|
+
self.write(Text.from_markup(f"[dim]{message}[/dim]"))
|
|
482
|
+
|
|
483
|
+
def write_welcome(self):
|
|
484
|
+
self.write(Text.from_markup(WELCOME_BANNER))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class SigmaApp(App):
|
|
488
|
+
"""Sigma Finance Research Agent."""
|
|
489
|
+
|
|
490
|
+
TITLE = "Sigma"
|
|
491
|
+
CSS = CSS
|
|
492
|
+
|
|
493
|
+
BINDINGS = [
|
|
494
|
+
Binding("ctrl+l", "clear", "Clear"),
|
|
495
|
+
Binding("ctrl+m", "models", "Models"),
|
|
496
|
+
Binding("ctrl+h", "help_toggle", "Help"),
|
|
497
|
+
Binding("ctrl+p", "palette", "palette", show=True),
|
|
498
|
+
Binding("escape", "cancel", show=False),
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
def __init__(self):
|
|
502
|
+
super().__init__()
|
|
503
|
+
self.settings = get_settings()
|
|
504
|
+
self.llm = None
|
|
505
|
+
self.conversation = []
|
|
506
|
+
self.is_processing = False
|
|
507
|
+
self.history: List[str] = []
|
|
508
|
+
self.history_idx = -1
|
|
509
|
+
self.show_help = False
|
|
510
|
+
|
|
511
|
+
def compose(self) -> ComposeResult:
|
|
512
|
+
yield Container(
|
|
513
|
+
ScrollableContainer(
|
|
514
|
+
ChatLog(id="chat-log", highlight=True, markup=True),
|
|
515
|
+
id="chat-area",
|
|
516
|
+
),
|
|
517
|
+
ToolCallDisplay(id="tool-calls-display"),
|
|
518
|
+
Static(id="help-panel"),
|
|
519
|
+
Container(
|
|
520
|
+
Horizontal(
|
|
521
|
+
SigmaIndicator(id="sigma-indicator"),
|
|
522
|
+
Input(
|
|
523
|
+
placeholder="Ask about any stock, market, or strategy... (Tab to autocomplete)",
|
|
524
|
+
id="prompt-input",
|
|
525
|
+
suggester=SigmaSuggester(),
|
|
526
|
+
),
|
|
527
|
+
TickerHighlight(id="ticker-highlight"),
|
|
528
|
+
id="input-row",
|
|
529
|
+
),
|
|
530
|
+
id="input-area",
|
|
531
|
+
),
|
|
532
|
+
id="main-container",
|
|
533
|
+
)
|
|
534
|
+
yield Footer()
|
|
535
|
+
|
|
536
|
+
def on_mount(self):
|
|
537
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
538
|
+
chat.write_welcome()
|
|
405
539
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
540
|
+
provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
|
|
541
|
+
chat.write_system(f"{SIGMA} Provider: {provider} | Model: {self.settings.default_model}")
|
|
542
|
+
chat.write_system(f"{SIGMA} Type /help for commands • Ctrl+H for quick help • Tab to autocomplete")
|
|
543
|
+
chat.write_system("")
|
|
410
544
|
|
|
411
|
-
|
|
545
|
+
self._init_llm()
|
|
546
|
+
self.query_one("#prompt-input", Input).focus()
|
|
412
547
|
|
|
413
|
-
def
|
|
414
|
-
|
|
415
|
-
|
|
548
|
+
def _init_llm(self):
|
|
549
|
+
try:
|
|
550
|
+
self.llm = get_llm(self.settings.default_provider, self.settings.default_model)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
553
|
+
chat.write_error(f"LLM init failed: {e}")
|
|
554
|
+
chat.write_system("Use /keys to configure API keys")
|
|
555
|
+
|
|
556
|
+
@on(Input.Changed)
|
|
557
|
+
def on_input_change(self, event: Input.Changed):
|
|
558
|
+
"""Update ticker highlight as user types."""
|
|
559
|
+
ticker_display = self.query_one("#ticker-highlight", TickerHighlight)
|
|
560
|
+
ticker_display.update_tickers(event.value)
|
|
561
|
+
|
|
562
|
+
@on(Input.Submitted)
|
|
563
|
+
def handle_input(self, event: Input.Submitted):
|
|
564
|
+
if self.is_processing:
|
|
416
565
|
return
|
|
417
566
|
|
|
418
|
-
|
|
419
|
-
|
|
567
|
+
text = event.value.strip()
|
|
568
|
+
if not text:
|
|
569
|
+
return
|
|
420
570
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
571
|
+
self.query_one("#prompt-input", Input).value = ""
|
|
572
|
+
self.history.append(text)
|
|
573
|
+
self.history_idx = len(self.history)
|
|
424
574
|
|
|
425
|
-
|
|
426
|
-
if '---' in line:
|
|
427
|
-
continue
|
|
428
|
-
cells = [c.strip() for c in line.split('|') if c.strip()]
|
|
429
|
-
if cells:
|
|
430
|
-
# Color code values
|
|
431
|
-
colored_cells = []
|
|
432
|
-
for cell in cells:
|
|
433
|
-
if cell.startswith('+') or cell.lower() in ['buy', 'bullish', 'strong buy']:
|
|
434
|
-
colored_cells.append(f"[green]{cell}[/green]")
|
|
435
|
-
elif cell.startswith('-') or cell.lower() in ['sell', 'bearish']:
|
|
436
|
-
colored_cells.append(f"[red]{cell}[/red]")
|
|
437
|
-
else:
|
|
438
|
-
colored_cells.append(cell)
|
|
439
|
-
table.add_row(*colored_cells)
|
|
575
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
440
576
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
async def quick_compare(self, symbols: list[str], period: str = "3mo"):
|
|
453
|
-
"""Generate a quick comparison chart."""
|
|
454
|
-
from sigma.tools.charts import create_comparison_chart
|
|
455
|
-
chart = create_comparison_chart(symbols, period)
|
|
456
|
-
console.print()
|
|
457
|
-
console.print(chart)
|
|
458
|
-
console.print()
|
|
459
|
-
|
|
460
|
-
def _handle_lean_command(self, args: list[str]):
|
|
461
|
-
"""Handle /lean commands for LEAN Engine backtesting."""
|
|
462
|
-
from sigma.tools.backtest import setup_lean_engine, run_lean_backtest, check_lean_status, get_available_strategies
|
|
577
|
+
if text.startswith("/"):
|
|
578
|
+
self._handle_command(text, chat)
|
|
579
|
+
else:
|
|
580
|
+
chat.write_user(text)
|
|
581
|
+
self._process_query(text, chat)
|
|
582
|
+
|
|
583
|
+
def _handle_command(self, cmd: str, chat: ChatLog):
|
|
584
|
+
parts = cmd.lower().split()
|
|
585
|
+
command = parts[0]
|
|
586
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
463
587
|
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
588
|
+
if command == "/help":
|
|
589
|
+
self._show_comprehensive_help(chat)
|
|
590
|
+
elif command == "/clear":
|
|
591
|
+
chat.clear()
|
|
592
|
+
self.conversation = []
|
|
593
|
+
chat.write_system("Chat cleared")
|
|
594
|
+
elif command == "/keys":
|
|
595
|
+
self._show_keys(chat)
|
|
596
|
+
elif command == "/models":
|
|
597
|
+
self._show_models(chat)
|
|
598
|
+
elif command == "/status":
|
|
599
|
+
self._show_status(chat)
|
|
600
|
+
elif command == "/backtest":
|
|
601
|
+
self._show_strategies(chat)
|
|
602
|
+
elif command == "/provider" and args:
|
|
603
|
+
self._switch_provider(args[0], chat)
|
|
604
|
+
elif command == "/model" and args:
|
|
605
|
+
self._switch_model(args[0], chat)
|
|
606
|
+
elif command.startswith("/setkey") and len(parts) >= 3:
|
|
607
|
+
self._set_key(parts[1], parts[2], chat)
|
|
608
|
+
elif command == "/tickers":
|
|
609
|
+
self._show_popular_tickers(chat)
|
|
610
|
+
else:
|
|
611
|
+
chat.write_error(f"Unknown command: {command}. Type /help for available commands.")
|
|
612
|
+
|
|
613
|
+
def _show_comprehensive_help(self, chat: ChatLog):
|
|
614
|
+
"""Show comprehensive help with examples."""
|
|
615
|
+
help_text = f"""
|
|
616
|
+
[bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
|
|
617
|
+
[bold] {SIGMA} SIGMA HELP CENTER [/bold]
|
|
618
|
+
[bold cyan]═══════════════════════════════════════════════════════════════[/bold cyan]
|
|
619
|
+
|
|
620
|
+
[bold yellow]QUICK START[/bold yellow]
|
|
621
|
+
Just type naturally! Examples:
|
|
622
|
+
• "analyze AAPL" - Full analysis of Apple
|
|
623
|
+
• "compare NVDA AMD INTC" - Compare multiple stocks
|
|
624
|
+
• "is TSLA overvalued?" - Get AI insights
|
|
625
|
+
• "market overview" - See major indices
|
|
626
|
+
|
|
627
|
+
[bold yellow]COMMANDS[/bold yellow]
|
|
628
|
+
[cyan]/help[/cyan] This help screen
|
|
629
|
+
[cyan]/clear[/cyan] Clear chat history
|
|
630
|
+
[cyan]/keys[/cyan] Configure API keys
|
|
631
|
+
[cyan]/models[/cyan] Show available models
|
|
632
|
+
[cyan]/status[/cyan] Current configuration
|
|
633
|
+
[cyan]/backtest[/cyan] Show backtest strategies
|
|
634
|
+
[cyan]/provider <name>[/cyan] Switch AI provider
|
|
635
|
+
[cyan]/model <name>[/cyan] Switch model
|
|
636
|
+
[cyan]/setkey <p> <k>[/cyan] Set API key
|
|
637
|
+
[cyan]/tickers[/cyan] Popular tickers list
|
|
638
|
+
|
|
639
|
+
[bold yellow]ANALYSIS EXAMPLES[/bold yellow]
|
|
640
|
+
• "technical analysis of SPY"
|
|
641
|
+
• "fundamentals of MSFT"
|
|
642
|
+
• "insider trading for AAPL"
|
|
643
|
+
• "analyst recommendations for NVDA"
|
|
644
|
+
• "sector performance"
|
|
645
|
+
|
|
646
|
+
[bold yellow]BACKTESTING[/bold yellow]
|
|
647
|
+
• "backtest SMA crossover on AAPL"
|
|
648
|
+
• "backtest RSI strategy on SPY"
|
|
649
|
+
• "backtest MACD on NVDA"
|
|
650
|
+
Strategies: sma_crossover, rsi, macd, bollinger, momentum, breakout
|
|
651
|
+
|
|
652
|
+
[bold yellow]KEYBOARD SHORTCUTS[/bold yellow]
|
|
653
|
+
[bold]Tab[/bold] Autocomplete suggestion
|
|
654
|
+
[bold]Ctrl+L[/bold] Clear chat
|
|
655
|
+
[bold]Ctrl+M[/bold] Show models
|
|
656
|
+
[bold]Ctrl+H[/bold] Toggle quick help
|
|
657
|
+
[bold]Ctrl+P[/bold] Command palette
|
|
658
|
+
[bold]Esc[/bold] Cancel operation
|
|
659
|
+
|
|
660
|
+
[bold yellow]TIPS[/bold yellow]
|
|
661
|
+
• Type [cyan]$AAPL[/cyan] or [cyan]AAPL[/cyan] - tickers auto-detected
|
|
662
|
+
• Use Tab for smart autocomplete
|
|
663
|
+
• Detected tickers shown next to input
|
|
664
|
+
"""
|
|
665
|
+
chat.write(Panel(
|
|
666
|
+
Text.from_markup(help_text),
|
|
667
|
+
title=f"[bold cyan]{SIGMA} Help[/bold cyan]",
|
|
668
|
+
border_style="cyan",
|
|
669
|
+
padding=(0, 1),
|
|
670
|
+
))
|
|
671
|
+
|
|
672
|
+
def _show_popular_tickers(self, chat: ChatLog):
|
|
673
|
+
"""Show popular tickers organized by category."""
|
|
674
|
+
tickers_text = """
|
|
675
|
+
[bold]Tech Giants[/bold]: AAPL, MSFT, GOOGL, AMZN, META, NVDA
|
|
676
|
+
[bold]Semiconductors[/bold]: NVDA, AMD, INTC, AVGO, QCOM, TSM
|
|
677
|
+
[bold]EVs & Auto[/bold]: TSLA, RIVN, LCID, F, GM
|
|
678
|
+
[bold]Finance[/bold]: JPM, BAC, GS, MS, V, MA
|
|
679
|
+
[bold]Healthcare[/bold]: JNJ, PFE, UNH, MRK, ABBV
|
|
680
|
+
[bold]ETFs[/bold]: SPY, QQQ, IWM, DIA, VTI, VOO
|
|
681
|
+
[bold]Sector ETFs[/bold]: XLK, XLF, XLE, XLV, XLI
|
|
682
|
+
"""
|
|
683
|
+
chat.write(Panel(
|
|
684
|
+
Text.from_markup(tickers_text),
|
|
685
|
+
title=f"[cyan]{SIGMA} Popular Tickers[/cyan]",
|
|
686
|
+
border_style="dim",
|
|
687
|
+
))
|
|
688
|
+
|
|
689
|
+
def _show_keys(self, chat: ChatLog):
|
|
690
|
+
chat.write_system(f"""
|
|
691
|
+
[bold]{SIGMA} API Keys[/bold]
|
|
692
|
+
Set key: /setkey <provider> <key>
|
|
693
|
+
|
|
694
|
+
Providers: google, openai, anthropic, groq, xai
|
|
695
|
+
Example: /setkey google AIzaSy...
|
|
696
|
+
""")
|
|
697
|
+
self._show_status(chat)
|
|
698
|
+
|
|
699
|
+
def _show_status(self, chat: ChatLog):
|
|
700
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
701
|
+
table.add_column("", style="bold")
|
|
702
|
+
table.add_column("")
|
|
703
|
+
|
|
704
|
+
provider = getattr(self.settings.default_provider, 'value', str(self.settings.default_provider))
|
|
705
|
+
table.add_row("Provider", provider)
|
|
706
|
+
table.add_row("Model", self.settings.default_model)
|
|
707
|
+
table.add_row("", "")
|
|
708
|
+
|
|
709
|
+
keys = [
|
|
710
|
+
("Google", self.settings.google_api_key),
|
|
711
|
+
("OpenAI", self.settings.openai_api_key),
|
|
712
|
+
("Anthropic", self.settings.anthropic_api_key),
|
|
713
|
+
("Groq", self.settings.groq_api_key),
|
|
714
|
+
("xAI", self.settings.xai_api_key),
|
|
715
|
+
]
|
|
716
|
+
for name, key in keys:
|
|
717
|
+
status = "[green]OK[/green]" if key else "[dim]--[/dim]"
|
|
718
|
+
table.add_row(f" {name}", Text.from_markup(status))
|
|
719
|
+
|
|
720
|
+
chat.write(Panel(table, title=f"[cyan]{SIGMA} Config[/cyan]", border_style="dim"))
|
|
721
|
+
|
|
722
|
+
def _show_models(self, chat: ChatLog):
|
|
723
|
+
table = Table(title=f"{SIGMA} Models", show_header=True, border_style="dim")
|
|
724
|
+
table.add_column("Provider", style="cyan")
|
|
725
|
+
table.add_column("Models")
|
|
726
|
+
for p, m in AVAILABLE_MODELS.items():
|
|
727
|
+
table.add_row(p, ", ".join(m))
|
|
728
|
+
chat.write(table)
|
|
729
|
+
|
|
730
|
+
def _show_strategies(self, chat: ChatLog):
|
|
731
|
+
strategies = get_available_strategies()
|
|
732
|
+
table = Table(title=f"{SIGMA} Strategies", show_header=True, border_style="dim")
|
|
733
|
+
table.add_column("Name", style="cyan")
|
|
734
|
+
table.add_column("Description")
|
|
735
|
+
for k, v in strategies.items():
|
|
736
|
+
table.add_row(k, v.get('description', ''))
|
|
737
|
+
chat.write(table)
|
|
738
|
+
|
|
739
|
+
def _switch_provider(self, provider: str, chat: ChatLog):
|
|
740
|
+
valid = ["google", "openai", "anthropic", "groq", "xai", "ollama"]
|
|
741
|
+
if provider not in valid:
|
|
742
|
+
chat.write_error(f"Invalid. Use: {', '.join(valid)}")
|
|
491
743
|
return
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
self.
|
|
744
|
+
try:
|
|
745
|
+
self.settings.default_provider = LLMProvider(provider)
|
|
746
|
+
if provider in AVAILABLE_MODELS:
|
|
747
|
+
self.settings.default_model = AVAILABLE_MODELS[provider][0]
|
|
748
|
+
self._init_llm()
|
|
749
|
+
chat.write_system(f"Switched to {provider}")
|
|
750
|
+
except Exception as e:
|
|
751
|
+
chat.write_error(str(e))
|
|
752
|
+
|
|
753
|
+
def _switch_model(self, model: str, chat: ChatLog):
|
|
754
|
+
self.settings.default_model = model
|
|
755
|
+
self._init_llm()
|
|
756
|
+
chat.write_system(f"Model: {model}")
|
|
757
|
+
|
|
758
|
+
def _set_key(self, provider: str, key: str, chat: ChatLog):
|
|
759
|
+
try:
|
|
760
|
+
save_api_key(LLMProvider(provider), key)
|
|
761
|
+
chat.write_system(f"{SIGMA} Key saved for {provider}")
|
|
762
|
+
if provider == getattr(self.settings.default_provider, 'value', ''):
|
|
763
|
+
self._init_llm()
|
|
764
|
+
except Exception as e:
|
|
765
|
+
chat.write_error(str(e))
|
|
766
|
+
|
|
767
|
+
@work(exclusive=True)
|
|
768
|
+
async def _process_query(self, query: str, chat: ChatLog):
|
|
769
|
+
if not self.llm:
|
|
770
|
+
chat.write_error("No LLM. Use /keys to configure.")
|
|
497
771
|
return
|
|
498
772
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
table = Table(title="Available Backtest Strategies", border_style="bright_blue")
|
|
504
|
-
table.add_column("Strategy", style="cyan")
|
|
505
|
-
table.add_column("Description", style="dim")
|
|
506
|
-
table.add_column("Parameters", style="yellow")
|
|
507
|
-
|
|
508
|
-
for name, s in strategies.items():
|
|
509
|
-
params = ", ".join([f"{k}={v}" for k, v in s.get("default_params", {}).items()])
|
|
510
|
-
table.add_row(name, s["description"][:60], params[:40])
|
|
511
|
-
|
|
512
|
-
console.print(table)
|
|
513
|
-
console.print()
|
|
514
|
-
return
|
|
773
|
+
self.is_processing = True
|
|
774
|
+
indicator = self.query_one("#sigma-indicator", SigmaIndicator)
|
|
775
|
+
tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
|
|
776
|
+
ticker_highlight = self.query_one("#ticker-highlight", TickerHighlight)
|
|
515
777
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
console.print(Panel(
|
|
520
|
-
"\n".join([
|
|
521
|
-
f"[dim]Docker Installed:[/dim] {'[green]Yes[/green]' if status['docker_installed'] else '[red]No[/red]'}",
|
|
522
|
-
f"[dim]Docker Running:[/dim] {'[green]Yes[/green]' if status['docker_running'] else '[red]No[/red]'}",
|
|
523
|
-
f"[dim]LEAN Image:[/dim] {'[green]Pulled[/green]' if status['lean_image_pulled'] else '[yellow]Not pulled[/yellow]'}",
|
|
524
|
-
f"[dim]Workspace:[/dim] {'[green]Ready[/green]' if status['workspace_initialized'] else '[yellow]Not initialized[/yellow]'}",
|
|
525
|
-
"",
|
|
526
|
-
*status['instructions']
|
|
527
|
-
]),
|
|
528
|
-
title="[bold bright_cyan]LEAN Engine Status[/bold bright_cyan]",
|
|
529
|
-
border_style="bright_blue"
|
|
530
|
-
))
|
|
531
|
-
console.print()
|
|
532
|
-
return
|
|
533
|
-
|
|
534
|
-
if subcmd == "setup":
|
|
535
|
-
console.print()
|
|
536
|
-
console.print("[bright_cyan]Setting up LEAN Engine...[/bright_cyan]")
|
|
537
|
-
console.print()
|
|
538
|
-
|
|
539
|
-
result = setup_lean_engine()
|
|
540
|
-
|
|
541
|
-
for step in result["steps_completed"]:
|
|
542
|
-
console.print(f" [green]✓[/green] {step}")
|
|
543
|
-
|
|
544
|
-
for error in result.get("errors", []):
|
|
545
|
-
console.print(f" [red]✗[/red] {error}")
|
|
546
|
-
|
|
547
|
-
console.print()
|
|
548
|
-
if result["success"]:
|
|
549
|
-
console.print(Panel(
|
|
550
|
-
"\n".join(result["next_steps"]),
|
|
551
|
-
title="[bold bright_green]Setup Complete[/bold bright_green]",
|
|
552
|
-
border_style="green"
|
|
553
|
-
))
|
|
554
|
-
else:
|
|
555
|
-
console.print(Panel(
|
|
556
|
-
"\n".join(result.get("next_steps", ["Setup failed. Check errors above."])),
|
|
557
|
-
title="[bold red]Setup Incomplete[/bold red]",
|
|
558
|
-
border_style="red"
|
|
559
|
-
))
|
|
560
|
-
console.print()
|
|
778
|
+
# Clear ticker highlight and start sigma animation
|
|
779
|
+
ticker_highlight.update("")
|
|
780
|
+
indicator.set_active(True)
|
|
561
781
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
return
|
|
567
|
-
|
|
568
|
-
symbol = args[1].upper()
|
|
569
|
-
strategy = args[2].lower()
|
|
570
|
-
|
|
571
|
-
console.print()
|
|
572
|
-
console.print(f"[bright_cyan]Running Comprehensive Backtest: {symbol} - {strategy}[/bright_cyan]")
|
|
573
|
-
console.print()
|
|
782
|
+
try:
|
|
783
|
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
784
|
+
messages.extend(self.conversation)
|
|
785
|
+
messages.append({"role": "user", "content": query})
|
|
574
786
|
|
|
575
|
-
|
|
787
|
+
all_tools = TOOLS + [BACKTEST_TOOL]
|
|
576
788
|
|
|
577
|
-
|
|
578
|
-
|
|
789
|
+
async def on_tool(name: str, args: dict):
|
|
790
|
+
tool_display.add_tool_call(name)
|
|
791
|
+
if name == "run_backtest":
|
|
792
|
+
result = run_backtest(**args)
|
|
793
|
+
else:
|
|
794
|
+
result = execute_tool(name, args)
|
|
795
|
+
tool_display.complete_tool_call(name)
|
|
796
|
+
return result
|
|
579
797
|
|
|
580
|
-
|
|
798
|
+
response = await self.llm.generate(messages, tools=all_tools, on_tool_call=on_tool)
|
|
581
799
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
# Performance metrics panel
|
|
586
|
-
perf = metrics.get("performance", {})
|
|
587
|
-
console.print(Panel(
|
|
588
|
-
"\n".join([
|
|
589
|
-
f" [bold cyan]Initial Capital:[/bold cyan] {perf.get('initial_capital', 'N/A')}",
|
|
590
|
-
f" [bold cyan]Final Equity:[/bold cyan] {perf.get('final_equity', 'N/A')}",
|
|
591
|
-
f" [bold cyan]Total Return:[/bold cyan] {perf.get('total_return', 'N/A')}",
|
|
592
|
-
f" [bold cyan]Annual Return:[/bold cyan] {perf.get('annual_return', 'N/A')}",
|
|
593
|
-
f" [bold cyan]Buy & Hold:[/bold cyan] {perf.get('buy_hold_return', 'N/A')}",
|
|
594
|
-
f" [bold cyan]Alpha:[/bold cyan] {perf.get('alpha', 'N/A')}",
|
|
595
|
-
]),
|
|
596
|
-
title=f"[bold bright_green]Performance - {symbol} {strategy}[/bold bright_green]",
|
|
597
|
-
border_style="green"
|
|
598
|
-
))
|
|
599
|
-
|
|
600
|
-
# Risk metrics panel
|
|
601
|
-
risk = metrics.get("risk", {})
|
|
602
|
-
console.print(Panel(
|
|
603
|
-
"\n".join([
|
|
604
|
-
f" [bold yellow]Max Drawdown:[/bold yellow] {risk.get('max_drawdown', 'N/A')}",
|
|
605
|
-
f" [bold yellow]Volatility:[/bold yellow] {risk.get('volatility', 'N/A')}",
|
|
606
|
-
f" [bold yellow]Sharpe Ratio:[/bold yellow] {risk.get('sharpe_ratio', 'N/A')}",
|
|
607
|
-
f" [bold yellow]Sortino Ratio:[/bold yellow] {risk.get('sortino_ratio', 'N/A')}",
|
|
608
|
-
f" [bold yellow]Calmar Ratio:[/bold yellow] {risk.get('calmar_ratio', 'N/A')}",
|
|
609
|
-
]),
|
|
610
|
-
title="[bold bright_yellow]Risk Metrics[/bold bright_yellow]",
|
|
611
|
-
border_style="yellow"
|
|
612
|
-
))
|
|
613
|
-
|
|
614
|
-
# Trade statistics panel
|
|
615
|
-
trades = metrics.get("trades", {})
|
|
616
|
-
console.print(Panel(
|
|
617
|
-
"\n".join([
|
|
618
|
-
f" [bold magenta]Total Trades:[/bold magenta] {trades.get('total_trades', 'N/A')}",
|
|
619
|
-
f" [bold magenta]Win Rate:[/bold magenta] {trades.get('win_rate', 'N/A')}",
|
|
620
|
-
f" [bold magenta]Profit Factor:[/bold magenta] {trades.get('profit_factor', 'N/A')}",
|
|
621
|
-
f" [bold magenta]Avg Win:[/bold magenta] {trades.get('avg_win', 'N/A')}",
|
|
622
|
-
f" [bold magenta]Avg Loss:[/bold magenta] {trades.get('avg_loss', 'N/A')}",
|
|
623
|
-
f" [bold magenta]Avg Holding:[/bold magenta] {trades.get('avg_holding_days', 'N/A')} days",
|
|
624
|
-
]),
|
|
625
|
-
title="[bold bright_magenta]Trade Statistics[/bold bright_magenta]",
|
|
626
|
-
border_style="magenta"
|
|
627
|
-
))
|
|
628
|
-
|
|
629
|
-
# Display charts
|
|
630
|
-
charts = result.get("charts", {})
|
|
631
|
-
|
|
632
|
-
if charts.get("equity_curve"):
|
|
633
|
-
console.print()
|
|
634
|
-
console.print(charts["equity_curve"])
|
|
635
|
-
|
|
636
|
-
if charts.get("drawdown"):
|
|
637
|
-
console.print()
|
|
638
|
-
console.print(charts["drawdown"])
|
|
639
|
-
|
|
640
|
-
if charts.get("trade_pnl"):
|
|
641
|
-
console.print()
|
|
642
|
-
console.print(charts["trade_pnl"])
|
|
643
|
-
|
|
644
|
-
if charts.get("monthly_returns"):
|
|
645
|
-
console.print()
|
|
646
|
-
console.print(charts["monthly_returns"])
|
|
647
|
-
|
|
648
|
-
# Recent trades table
|
|
649
|
-
trade_list = result.get("trades", [])
|
|
650
|
-
if trade_list:
|
|
651
|
-
console.print()
|
|
652
|
-
from rich.table import Table
|
|
653
|
-
table = Table(title="Recent Trades", border_style="dim")
|
|
654
|
-
table.add_column("Date", style="dim")
|
|
655
|
-
table.add_column("Action", style="bold")
|
|
656
|
-
table.add_column("Price", justify="right")
|
|
657
|
-
table.add_column("Shares", justify="right")
|
|
658
|
-
table.add_column("P&L", justify="right")
|
|
659
|
-
|
|
660
|
-
for trade in trade_list[-10:]:
|
|
661
|
-
action_style = "green" if trade.get("action") == "BUY" else "red"
|
|
662
|
-
pnl = trade.get("pnl")
|
|
663
|
-
pnl_str = f"${pnl:+,.2f}" if pnl else "-"
|
|
664
|
-
pnl_style = "green" if pnl and pnl > 0 else "red" if pnl else "dim"
|
|
665
|
-
table.add_row(
|
|
666
|
-
trade.get("date", ""),
|
|
667
|
-
f"[{action_style}]{trade.get('action', '')}[/{action_style}]",
|
|
668
|
-
f"${trade.get('price', 0):,.2f}",
|
|
669
|
-
str(trade.get("shares", "")),
|
|
670
|
-
f"[{pnl_style}]{pnl_str}[/{pnl_style}]"
|
|
671
|
-
)
|
|
672
|
-
console.print(table)
|
|
673
|
-
|
|
674
|
-
# Monthly returns
|
|
675
|
-
monthly = result.get("monthly_returns", [])
|
|
676
|
-
if monthly:
|
|
677
|
-
console.print()
|
|
678
|
-
from rich.table import Table
|
|
679
|
-
mtable = Table(title="Monthly Returns", border_style="dim")
|
|
680
|
-
mtable.add_column("Month", style="dim")
|
|
681
|
-
mtable.add_column("Return", justify="right")
|
|
682
|
-
|
|
683
|
-
for m in monthly[-12:]:
|
|
684
|
-
ret = m.get("return", 0)
|
|
685
|
-
ret_style = "green" if ret > 0 else "red"
|
|
686
|
-
mtable.add_row(m.get("month", ""), f"[{ret_style}]{ret:+.2f}%[/{ret_style}]")
|
|
687
|
-
console.print(mtable)
|
|
688
|
-
|
|
689
|
-
# QuantConnect instructions
|
|
690
|
-
console.print()
|
|
691
|
-
console.print(Panel(
|
|
692
|
-
"\n".join(result.get("quantconnect_instructions", [])),
|
|
693
|
-
title="[bold bright_blue]QuantConnect Cloud (Institutional Data)[/bold bright_blue]",
|
|
694
|
-
border_style="blue"
|
|
695
|
-
))
|
|
696
|
-
|
|
697
|
-
else:
|
|
698
|
-
console.print(Panel(
|
|
699
|
-
f"[red]Error:[/red] {result.get('error', 'Unknown error')[:200]}",
|
|
700
|
-
title="[bold red]Backtest Failed[/bold red]",
|
|
701
|
-
border_style="red"
|
|
702
|
-
))
|
|
800
|
+
# Clear tool display after getting response
|
|
801
|
+
await asyncio.sleep(0.5) # Brief pause to show completion
|
|
802
|
+
tool_display.clear()
|
|
703
803
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
elif subcmd == "status":
|
|
711
|
-
self._handle_lean_command([]) # Same as no args
|
|
712
|
-
|
|
713
|
-
else:
|
|
714
|
-
console.print(f"\n [red]Unknown lean command:[/red] {subcmd}")
|
|
715
|
-
console.print(" [dim]Available:[/dim] /lean setup, /lean run <symbol> <strategy>, /lean status\n")
|
|
716
|
-
|
|
717
|
-
def handle_command(self, cmd: str) -> bool:
|
|
718
|
-
"""Handle slash command. Returns True to continue, False to quit."""
|
|
719
|
-
parts = cmd.strip().split()
|
|
720
|
-
command = parts[0].lower()
|
|
721
|
-
args = parts[1:] if len(parts) > 1 else []
|
|
722
|
-
|
|
723
|
-
if command in ["/quit", "/exit", "/q"]:
|
|
724
|
-
return False
|
|
725
|
-
|
|
726
|
-
elif command in ["/help", "/h", "/?"]:
|
|
727
|
-
print_help()
|
|
728
|
-
|
|
729
|
-
elif command == "/model":
|
|
730
|
-
if not args:
|
|
731
|
-
available = self.settings.get_available_providers()
|
|
732
|
-
console.print()
|
|
733
|
-
console.print(f" [dim]Current model:[/dim] [bright_cyan]{self._get_model_display()}[/bright_cyan]")
|
|
734
|
-
console.print(f" [dim]Available:[/dim] {', '.join(p.value for p in available)}")
|
|
735
|
-
console.print()
|
|
736
|
-
else:
|
|
737
|
-
try:
|
|
738
|
-
self.provider = LLMProvider(args[0].lower())
|
|
739
|
-
self.agent = None
|
|
740
|
-
console.print(f"\n [bright_green]✓[/bright_green] Switched to [bright_cyan]{self._get_model_display()}[/bright_cyan]\n")
|
|
741
|
-
except ValueError:
|
|
742
|
-
console.print(f"\n [red]✗[/red] Unknown provider: {args[0]}\n")
|
|
743
|
-
|
|
744
|
-
elif command == "/mode":
|
|
745
|
-
if not args:
|
|
746
|
-
console.print()
|
|
747
|
-
console.print(f" [dim]Current mode:[/dim] [bright_cyan]{self.mode}[/bright_cyan]")
|
|
748
|
-
console.print(f" [dim]Available:[/dim] default, technical, fundamental, quant")
|
|
749
|
-
console.print()
|
|
750
|
-
else:
|
|
751
|
-
mode = args[0].lower()
|
|
752
|
-
if mode in ["default", "technical", "fundamental", "quant"]:
|
|
753
|
-
self.mode = mode
|
|
754
|
-
console.print(f"\n [bright_green]✓[/bright_green] Switched to [bright_cyan]{mode}[/bright_cyan] mode\n")
|
|
755
|
-
else:
|
|
756
|
-
console.print(f"\n [red]✗[/red] Unknown mode: {mode}\n")
|
|
757
|
-
|
|
758
|
-
elif command == "/clear":
|
|
759
|
-
if self.agent:
|
|
760
|
-
self.agent.clear()
|
|
761
|
-
console.print("\n [bright_green]✓[/bright_green] Conversation cleared\n")
|
|
762
|
-
|
|
763
|
-
elif command == "/status":
|
|
764
|
-
console.print()
|
|
765
|
-
console.print(f" [dim]Provider:[/dim] [bright_cyan]{self.provider.value}[/bright_cyan]")
|
|
766
|
-
console.print(f" [dim]Model:[/dim] [bright_cyan]{self._get_model_display()}[/bright_cyan]")
|
|
767
|
-
console.print(f" [dim]Mode:[/dim] [bright_cyan]{self.mode}[/bright_cyan]")
|
|
768
|
-
available = self.settings.get_available_providers()
|
|
769
|
-
console.print(f" [dim]Available providers:[/dim] {', '.join(p.value for p in available)}")
|
|
770
|
-
if self.agent:
|
|
771
|
-
stats = self.agent.get_stats()
|
|
772
|
-
console.print(f" [dim]Tools called this session:[/dim] {stats['tools_called']}")
|
|
773
|
-
console.print()
|
|
774
|
-
|
|
775
|
-
elif command == "/chart":
|
|
776
|
-
if not args:
|
|
777
|
-
console.print("\n [red]Usage:[/red] /chart <symbol> [period]\n")
|
|
804
|
+
if response:
|
|
805
|
+
chat.write_assistant(response)
|
|
806
|
+
self.conversation.append({"role": "user", "content": query})
|
|
807
|
+
self.conversation.append({"role": "assistant", "content": response})
|
|
808
|
+
if len(self.conversation) > 20:
|
|
809
|
+
self.conversation = self.conversation[-20:]
|
|
778
810
|
else:
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
try:
|
|
804
|
-
loop = asyncio.get_event_loop()
|
|
805
|
-
loop.run_until_complete(
|
|
806
|
-
self.process_query(f"Generate a {strategy} backtest for {symbol}")
|
|
807
|
-
)
|
|
808
|
-
except RuntimeError:
|
|
809
|
-
asyncio.run(
|
|
810
|
-
self.process_query(f"Generate a {strategy} backtest for {symbol}")
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
elif command == "/lean":
|
|
814
|
-
self._handle_lean_command(args)
|
|
815
|
-
|
|
811
|
+
chat.write_error("No response")
|
|
812
|
+
except Exception as e:
|
|
813
|
+
tool_display.clear()
|
|
814
|
+
chat.write_error(str(e))
|
|
815
|
+
finally:
|
|
816
|
+
indicator.set_active(False)
|
|
817
|
+
self.is_processing = False
|
|
818
|
+
self.query_one("#prompt-input", Input).focus()
|
|
819
|
+
|
|
820
|
+
def action_clear(self):
|
|
821
|
+
chat = self.query_one("#chat-log", ChatLog)
|
|
822
|
+
chat.clear()
|
|
823
|
+
self.conversation = []
|
|
824
|
+
chat.write_system("Cleared")
|
|
825
|
+
|
|
826
|
+
def action_models(self):
|
|
827
|
+
self._show_models(self.query_one("#chat-log", ChatLog))
|
|
828
|
+
|
|
829
|
+
def action_help_toggle(self):
|
|
830
|
+
"""Toggle quick help panel."""
|
|
831
|
+
help_panel = self.query_one("#help-panel", Static)
|
|
832
|
+
if self.show_help:
|
|
833
|
+
help_panel.remove_class("visible")
|
|
834
|
+
help_panel.update("")
|
|
816
835
|
else:
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
836
|
+
help_panel.add_class("visible")
|
|
837
|
+
help_panel.update(Text.from_markup(
|
|
838
|
+
"[bold]Quick Commands:[/bold] /help /clear /keys /models /status /backtest "
|
|
839
|
+
"[bold]Shortcuts:[/bold] Tab=autocomplete Ctrl+L=clear Ctrl+M=models"
|
|
840
|
+
))
|
|
841
|
+
self.show_help = not self.show_help
|
|
820
842
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if not query:
|
|
832
|
-
continue
|
|
833
|
-
|
|
834
|
-
# Handle commands
|
|
835
|
-
if query.startswith("/"):
|
|
836
|
-
if not self.handle_command(query):
|
|
837
|
-
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim] 📈\n")
|
|
838
|
-
break
|
|
839
|
-
continue
|
|
840
|
-
|
|
841
|
-
# Process query
|
|
842
|
-
await self.process_query(query)
|
|
843
|
-
|
|
844
|
-
except KeyboardInterrupt:
|
|
845
|
-
console.print("\n")
|
|
846
|
-
continue
|
|
847
|
-
except EOFError:
|
|
848
|
-
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim] 📈\n")
|
|
849
|
-
break
|
|
850
|
-
except Exception as e:
|
|
851
|
-
console.print(f"\n [red]Error:[/red] {str(e)}\n")
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
def main():
|
|
855
|
-
"""Main entry point."""
|
|
856
|
-
import argparse
|
|
857
|
-
|
|
858
|
-
parser = argparse.ArgumentParser(
|
|
859
|
-
prog="sigma",
|
|
860
|
-
description="Sigma - Institutional-Grade Financial Research Agent",
|
|
861
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
862
|
-
epilog="""
|
|
863
|
-
Examples:
|
|
864
|
-
sigma Start interactive mode
|
|
865
|
-
sigma --setup Run setup wizard
|
|
866
|
-
sigma "Analyze AAPL" Direct query mode
|
|
867
|
-
sigma --version Show version
|
|
868
|
-
|
|
869
|
-
Inside Sigma:
|
|
870
|
-
/help Show all commands
|
|
871
|
-
/model openai Switch to OpenAI
|
|
872
|
-
/lean run TSLA macd Run backtest
|
|
873
|
-
"""
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
parser.add_argument(
|
|
877
|
-
"query",
|
|
878
|
-
nargs="?",
|
|
879
|
-
help="Direct query to analyze (optional)"
|
|
880
|
-
)
|
|
881
|
-
parser.add_argument(
|
|
882
|
-
"--setup",
|
|
883
|
-
action="store_true",
|
|
884
|
-
help="Run the setup wizard"
|
|
885
|
-
)
|
|
886
|
-
parser.add_argument(
|
|
887
|
-
"--reset",
|
|
888
|
-
action="store_true",
|
|
889
|
-
help="Reset configuration and run setup"
|
|
890
|
-
)
|
|
891
|
-
parser.add_argument(
|
|
892
|
-
"--version", "-v",
|
|
893
|
-
action="version",
|
|
894
|
-
version=f"Sigma {VERSION}"
|
|
895
|
-
)
|
|
896
|
-
parser.add_argument(
|
|
897
|
-
"--model", "-m",
|
|
898
|
-
choices=["openai", "anthropic", "google", "groq", "xai", "ollama"],
|
|
899
|
-
help="Override default AI model"
|
|
900
|
-
)
|
|
901
|
-
|
|
902
|
-
args = parser.parse_args()
|
|
903
|
-
|
|
904
|
-
# Handle setup
|
|
905
|
-
from sigma.setup import ensure_setup, run_setup, is_setup_complete, CONFIG_DIR
|
|
906
|
-
import shutil
|
|
907
|
-
|
|
908
|
-
if args.reset:
|
|
909
|
-
if CONFIG_DIR.exists():
|
|
910
|
-
shutil.rmtree(CONFIG_DIR)
|
|
911
|
-
run_setup(force=True)
|
|
912
|
-
elif args.setup:
|
|
913
|
-
run_setup(force=True)
|
|
914
|
-
else:
|
|
915
|
-
# Ensure setup is done
|
|
916
|
-
ensure_setup()
|
|
917
|
-
|
|
918
|
-
# Create UI with optional model override
|
|
919
|
-
ui = SigmaUI()
|
|
920
|
-
if args.model:
|
|
921
|
-
try:
|
|
922
|
-
ui.provider = LLMProvider(args.model)
|
|
923
|
-
ui.agent = None # Force agent reload
|
|
924
|
-
except ValueError:
|
|
925
|
-
pass
|
|
926
|
-
|
|
927
|
-
# Handle direct query or interactive mode
|
|
928
|
-
if args.query:
|
|
929
|
-
# Direct query mode
|
|
930
|
-
async def run_query():
|
|
931
|
-
print_banner(ui._get_model_display())
|
|
932
|
-
await ui.process_query(args.query)
|
|
933
|
-
|
|
934
|
-
try:
|
|
935
|
-
asyncio.run(run_query())
|
|
936
|
-
except KeyboardInterrupt:
|
|
937
|
-
console.print("\n")
|
|
938
|
-
else:
|
|
939
|
-
# Interactive mode
|
|
940
|
-
try:
|
|
941
|
-
asyncio.run(ui.run())
|
|
942
|
-
except KeyboardInterrupt:
|
|
943
|
-
console.print("\n [dim]Goodbye! May your trades be ever profitable.[/dim]\n")
|
|
843
|
+
def action_cancel(self):
|
|
844
|
+
if self.is_processing:
|
|
845
|
+
self.is_processing = False
|
|
846
|
+
tool_display = self.query_one("#tool-calls-display", ToolCallDisplay)
|
|
847
|
+
tool_display.clear()
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def launch():
|
|
851
|
+
"""Launch Sigma."""
|
|
852
|
+
SigmaApp().run()
|
|
944
853
|
|
|
945
854
|
|
|
946
855
|
if __name__ == "__main__":
|
|
947
|
-
|
|
856
|
+
launch()
|